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


import pygame
import math
import random
import sys
import json
import os

pygame.init()

# ---------- Поле ----------
WIDTH, HEIGHT = 1100, 750
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Рогалик")
clock = pygame.time.Clock()
FONT = pygame.font.SysFont("Arial", 22, bold=True)
BIG_FONT = pygame.font.SysFont("Arial", 34, bold=True)
HUGE_FONT = pygame.font.SysFont("Arial", 48, bold=True)
SMALL_FONT = pygame.font.SysFont("Arial", 16)
TINY_FONT = pygame.font.SysFont("Arial", 14)

SAVE_FILE = "roguelike_save.json"

# ---------- Настройки ----------
PLAYER_RADIUS = 16
ZOMBIE_RADIUS = 13
PHANTOM_RADIUS = 9
FAT_RADIUS = 26
SUMMONER_RADIUS = 18
TANK_RADIUS = 20
SPLITTER_RADIUS = 15
PLAYER_SPEED = 4
ZOMBIE_BASE_SPEED = 1.2
PHANTOM_SPEED_MULT = 1.9
FAT_SPEED_MULT = 0.55
SUMMONER_SPEED_MULT = 0.7
TANK_SPEED_MULT = 0.45
SPLITTER_SPEED_MULT = 1.1
FAT_HP_BASE = 8
FAT_EXPLOSION_RADIUS = 90
FAT_EXPLOSION_DAMAGE = 3
WAVE_DELAY = 5.0
ATTACK_COOLDOWN = 2.0
ATTACK_RANGE = 140
ATTACK_WIDTH = 26
SWING_TIME = 0.18
PHANTOM_START_WAVE = 5
FAT_START_WAVE = 7
SPLITTER_START_WAVE = 8
SUMMONER_START_WAVE = 10
TANK_START_WAVE = 13
GROUP_SPAWN_START_WAVE = 6
GROUP_SPAWN_TIME = 4.0
HUD_SHOW_TIME = 3.0

SUMMONER_HP_BASE = 4
SUMMONER_SUMMON_CD = 8.0
SUMMONER_MAX_PHANTOMS = 3
SUMMONER_SUMMON_RADIUS = 34

TANK_HP_BASE = 14
TANK_DAMAGE = 2
SPLITTER_HP_BASE = 3
SPLITTER_CHILDREN = 3

MAX_PARTICLES = 400
CHAOS_EVERY = 3

# ---------- Настройки монет ----------
MONEY_KILL_MIN = 1
MONEY_KILL_MAX = 10
MONEY_PHANTOM = 5
MONEY_FAT_MIN = 11
MONEY_FAT_MAX = 22
MONEY_SUMMONER_MIN = 25
MONEY_SUMMONER_MAX = 40
MONEY_TANK_MIN = 30
MONEY_TANK_MAX = 50
MONEY_SPLITTER_MIN = 8
MONEY_SPLITTER_MAX = 15
MONEY_WAVE_BASE = 50
MONEY_WAVE_GROWTH = 15


# ---------- Скины ----------
SKINS = [
    {"name": "Синий",      "color": (80, 180, 255),  "glow": (120, 200, 255), "price": 0},
    {"name": "Красный",    "color": (255, 90, 90),   "glow": (255, 130, 130), "price": 0},
    {"name": "Зелёный",    "color": (110, 220, 110), "glow": (160, 255, 160), "price": 0},
    {"name": "Жёлтый",     "color": (255, 220, 100), "glow": (255, 240, 150), "price": 0},
    {"name": "Фиолетовый", "color": (200, 120, 255), "glow": (230, 170, 255), "price": 0},
    {"name": "Золотой",    "color": (255, 200, 60),  "glow": (255, 240, 100), "price": 300},
    {"name": "Кровавый",   "color": (180, 30, 30),   "glow": (255, 80, 80),   "price": 500},
    {"name": "Ледяной",    "color": (150, 240, 255), "glow": (200, 255, 255), "price": 700},
    {"name": "Тёмный",     "color": (60, 60, 90),    "glow": (150, 150, 200), "price": 1000},
    {"name": "Радужный",   "color": (255, 100, 200), "glow": (150, 255, 255), "price": 1500, "rainbow": True},
]


# ---------- Перки ----------
PERKS = [
    {"id": "dash",          "name": "Рывок",           "price": 800,
     "desc": "ПРОБЕЛ — рывок в сторону курсора. Во время рывка неуязвим. КД 3 сек.",
     "color": (120, 220, 255)},
    {"id": "second_wind",   "name": "Второе дыхание",  "price": 1200,
     "desc": "Один раз за волну при смерти восстанавливает 3 HP.",
     "color": (120, 255, 160)},
    {"id": "bloodthirst",   "name": "Кровожадность",   "price": 900,
     "desc": "+10% шанс крита за убийство в волне (макс +50%).",
     "color": (255, 80, 80)},
    {"id": "magnet",        "name": "Магнит",          "price": 600,
     "desc": "Пикапы здоровья притягиваются к тебе.",
     "color": (140, 255, 200)},
    {"id": "thorns",        "name": "Шипы",            "price": 1000,
     "desc": "Враги, коснувшиеся тебя, получают 1 урон.",
     "color": (200, 150, 255)},
    {"id": "explosive",     "name": "Осколки",         "price": 1300,
     "desc": "Убитые враги с шансом 25% взрываются.",
     "color": (255, 140, 60)},
    {"id": "lucky",         "name": "Счастливчик",     "price": 700,
     "desc": "+20% монет за убийство.",
     "color": (255, 220, 120)},
    {"id": "vampire",       "name": "Вампир",          "price": 1100,
     "desc": "15% шанс вылечиться на 1 при убийстве.",
     "color": (255, 100, 140)},
]


# ---------- Сохранение ----------
def load_save():
    default = {
        "money": 0,
        "owned_skins": [0, 1, 2, 3, 4],
        "selected_skin": 0,
        "owned_perks": [],
        "equipped_perks": [],
        "admin_unlocked": False,
    }
    if os.path.exists(SAVE_FILE):
        try:
            with open(SAVE_FILE, "r", encoding="utf-8") as f:
                data = json.load(f)
            for k, v in default.items():
                if k not in data:
                    data[k] = v
            return data
        except Exception:
            return default
    return default


def save_game(data):
    try:
        with open(SAVE_FILE, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
    except Exception:
        pass


# ---------- Вспомогательные ----------
def draw_glow_circle(surf, color, pos, radius, glow_color=None):
    if glow_color is None:
        glow_color = color
    glow_surf = pygame.Surface((radius * 4, radius * 4), pygame.SRCALPHA)
    for i in range(6, 0, -1):
        alpha = int(30 * (1 - i / 6))
        pygame.draw.circle(glow_surf, (*glow_color, alpha),
                           (radius * 2, radius * 2), radius + i * 2)
    surf.blit(glow_surf, (pos[0] - radius * 2, pos[1] - radius * 2))
    pygame.draw.circle(surf, color, pos, radius)


def spawn_particles(lst, x, y, color, count, speed, life, size):
    room = MAX_PARTICLES - len(lst)
    if room <= 0:
        return
    n = min(count, room)
    for _ in range(n):
        lst.append(Particle(x, y, color, speed=speed, life=life, size=size))


def wrap_text(text, font, max_width):
    words = text.split()
    lines = []
    cur = ""
    for w in words:
        test = (cur + " " + w).strip()
        if font.size(test)[0] <= max_width:
            cur = test
        else:
            if cur:
                lines.append(cur)
            cur = w
    if cur:
        lines.append(cur)
    return lines


def render_fit(text, max_width, max_height, color, preferred_font=None):
    sizes = []
    if preferred_font is not None:
        sizes.append(preferred_font)
    sizes += [pygame.font.SysFont("Arial", s, bold=True) for s in (20, 18, 16, 14, 12)]
    for font in sizes:
        lines = wrap_text(text, font, max_width)
        total_h = len(lines) * font.get_linesize()
        if total_h <= max_height:
            return [font.render(ln, True, color) for ln in lines]
    font = pygame.font.SysFont("Arial", 12, bold=True)
    lines = wrap_text(text, font, max_width)
    return [font.render(ln, True, color) for ln in lines]


# ---------- Объекты ----------
class HitMarker:
    def __init__(self, x, y, text, color=(255, 240, 100)):
        self.x, self.y, self.text, self.color = x, y, text, color
        self.life = self.max_life = 0.8
        self.vx = random.uniform(-20, 20)
        self.vy = -60

    def update(self, dt):
        self.x += self.vx * dt
        self.y += self.vy * dt
        self.vy += 100 * dt
        self.life -= dt
        return self.life > 0

    def draw(self, surf):
        t = max(0, self.life / self.max_life)
        alpha = int(255 * t)
        size = int(20 + 8 * (1 - t))
        font = pygame.font.SysFont("Arial", size, bold=True)
        img = font.render(self.text, True, self.color)
        img.set_alpha(alpha)
        surf.blit(img, (self.x - img.get_width() // 2, self.y))


class Particle:
    __slots__ = ("x", "y", "color", "size", "vx", "vy", "life", "max_life")

    def __init__(self, x, y, color, speed=120, life=0.5, size=4):
        self.x, self.y, self.color, self.size = x, y, color, size
        ang = random.uniform(0, math.tau)
        sp = random.uniform(speed * 0.3, speed)
        self.vx = math.cos(ang) * sp
        self.vy = math.sin(ang) * sp
        self.life = self.max_life = life

    def update(self, dt):
        self.x += self.vx * dt
        self.y += self.vy * dt
        self.vx *= 0.92
        self.vy *= 0.92
        self.life -= dt
        return self.life > 0

    def draw(self, surf):
        t = max(0, self.life / self.max_life)
        r = max(1, int(self.size * t))
        pygame.draw.circle(surf, self.color, (int(self.x), int(self.y)), r)


class SlashEffect:
    def __init__(self, x, y, angle, length, width):
        self.x, self.y, self.angle = x, y, angle
        self.length, self.width = length, width
        self.life = self.max_life = SWING_TIME

    def update(self, dt):
        self.life -= dt
        return self.life > 0

    def draw(self, surf):
        t = max(0, self.life / self.max_life)
        progress = 1.0 - t
        cur_len = self.length * min(1.0, progress * 1.6)
        alpha = int(255 * t)
        w = max(2, int(self.width * (0.4 + 0.6 * t)))
        half = w / 2
        ex = self.x + math.cos(self.angle) * cur_len
        ey = self.y + math.sin(self.angle) * cur_len
        px = -math.sin(self.angle) * half
        py = math.cos(self.angle) * half
        p1 = (self.x + px, self.y + py)
        p2 = (self.x - px, self.y - py)
        p3 = (ex - px, ey - py)
        p4 = (ex + px, ey + py)

        glow = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
        for g in range(4, 0, -1):
            gw = half + g * 3
            gpx = -math.sin(self.angle) * gw
            gpy = math.cos(self.angle) * gw
            a = int(alpha * 0.15 * (g / 4))
            pygame.draw.polygon(glow, (255, 220, 120, a),
                                [(self.x + gpx, self.y + gpy),
                                 (self.x - gpx, self.y - gpy),
                                 (ex - gpx, ey - gpy),
                                 (ex + gpx, ey + gpy)])
        surf.blit(glow, (0, 0), special_flags=pygame.BLEND_RGBA_ADD)

        core = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
        pygame.draw.polygon(core, (255, 255, 220, alpha), [p1, p2, p3, p4])
        inner = max(1, w // 3)
        ipx = -math.sin(self.angle) * inner / 2
        ipy = math.cos(self.angle) * inner / 2
        pygame.draw.polygon(core, (255, 255, 255, alpha),
                            [(self.x + ipx, self.y + ipy),
                             (self.x - ipx, self.y - ipy),
                             (ex - ipx, ey - ipy),
                             (ex + ipx, ey + ipy)])
        surf.blit(core, (0, 0), special_flags=pygame.BLEND_RGBA_ADD)
        pygame.draw.circle(surf, (255, 255, 220), (int(ex), int(ey)), max(2, int(6 * t)))


class Projectile:
    def __init__(self, x, y, angle, speed, damage, pierce, player, max_range=700):
        self.x, self.y = x, y
        self.angle = angle
        self.vx = math.cos(angle) * speed
        self.vy = math.sin(angle) * speed
        self.radius = 6
        self.damage = damage
        self.pierce = pierce
        self.hit_ids = set()
        self.player = player
        self.traveled = 0.0
        self.max_range = max_range
        self.trail = []

    def update(self, dt, zombies, particles, hit_markers):
        step = math.hypot(self.vx * dt, self.vy * dt)
        self.x += self.vx * dt
        self.y += self.vy * dt
        self.traveled += step

        self.trail.append([self.x, self.y, 0.22])
        self.trail = [t for t in self.trail if t[2] - dt > 0]
        for t in self.trail:
            t[2] -= dt

        if not (-50 < self.x < WIDTH + 50 and -50 < self.y < HEIGHT + 50):
            return False
        if self.traveled > self.max_range:
            return False

        for z in zombies[:]:
            if id(z) in self.hit_ids:
                continue
            if math.hypot(z.x - self.x, z.y - self.y) < self.radius + z.radius:
                crit = random.random() < getattr(self.player, "crit", 0)
                crit_mult = getattr(self.player, "crit_mult", 2.0)
                dmg = self.damage * (crit_mult if crit else 1)
                z.hp -= dmg
                z.hit_timer = 0.15
                color = (255, 100, 100) if crit else (255, 240, 100)
                text = f"{int(dmg)}" + ("!" if crit else "")
                hit_markers.append(HitMarker(z.x, z.y - z.radius - 5, text, color))
                self.hit_ids.add(id(z))
                kb = self.player.knockback
                z.knock_vx += math.cos(self.angle) * kb * 3
                z.knock_vy += math.sin(self.angle) * kb * 3
                spawn_particles(particles, z.x, z.y, (220, 60, 60), 3, 150, 0.3, 3)
                if self.pierce <= 0:
                    return False
                self.pierce -= 1
        return True

    def draw(self, surf):
        for tx, ty, t in self.trail:
            a = int(180 * (t / 0.22))
            r = max(1, int(self.radius * (t / 0.22)))
            s = pygame.Surface((r * 2 + 2, r * 2 + 2), pygame.SRCALPHA)
            pygame.draw.circle(s, (255, 220, 120, a), (r + 1, r + 1), r)
            surf.blit(s, (tx - r - 1, ty - r - 1))
        pygame.draw.circle(surf, (255, 240, 160), (int(self.x), int(self.y)), self.radius)
        pygame.draw.circle(surf, (255, 255, 255), (int(self.x), int(self.y)), self.radius - 2)


class Player:
    def __init__(self, player_class="melee", skin=None, perks=None):
        self.player_class = player_class
        self.skin = skin or SKINS[0]
        self.perks = perks or []
        self.x, self.y = WIDTH // 2, HEIGHT // 2
        self.radius = PLAYER_RADIUS
        self.hp = self.max_hp = 5
        self.damage = 1
        self.attack_cooldown = ATTACK_COOLDOWN
        self.attack_timer = 0.0
        self.attack_range = ATTACK_RANGE
        self.attack_width = ATTACK_WIDTH
        self.aim_angle = 0.0
        self.speed = PLAYER_SPEED
        self.hit_flash = 0.0
        self.bob = 0.0
        self.knockback = 18
        self.lifesteal = 0
        self.regen = 0.0
        self.regen_acc = 0.0
        self.armor = 0
        self.projectile_speed = 750
        self.projectile_pierce = 0
        self.multishot = 1
        self.attack_range_max = 600
        # Перковые поля
        self.dash_cd = 0.0
        self.dash_cd_max = 3.0
        self.dash_timer = 0.0
        self.dash_dir = (0, 0)
        self.invulnerable = False
        self.second_wind_used = False
        self.bloodthirst_bonus = 0.0

    def has_perk(self, pid):
        return pid in self.perks

    def update_aim(self):
        mx, my = pygame.mouse.get_pos()
        self.aim_angle = math.atan2(my - self.y, mx - self.x)

    def move(self, keys, dt):
        # рывок перекрывает обычное движение
        if self.dash_timer > 0:
            self.x += self.dash_dir[0] * 22 * dt * 60
            self.y += self.dash_dir[1] * 22 * dt * 60
            self.dash_timer -= dt
            if self.dash_timer <= 0:
                self.invulnerable = False
            self.x = max(self.radius, min(WIDTH - self.radius, self.x))
            self.y = max(self.radius, min(HEIGHT - self.radius, self.y))
            return

        dx = dy = 0
        if keys[pygame.K_a] or keys[pygame.K_LEFT]: dx -= 1
        if keys[pygame.K_d] or keys[pygame.K_RIGHT]: dx += 1
        if keys[pygame.K_w] or keys[pygame.K_UP]: dy -= 1
        if keys[pygame.K_s] or keys[pygame.K_DOWN]: dy += 1
        if dx or dy:
            l = math.hypot(dx, dy)
            self.x += dx / l * self.speed * dt * 60
            self.y += dy / l * self.speed * dt * 60
            self.bob += dt * 15
        self.x = max(self.radius, min(WIDTH - self.radius, self.x))
        self.y = max(self.radius, min(HEIGHT - self.radius, self.y))

    def try_dash(self):
        if not self.has_perk("dash"):
            return False
        if self.dash_cd > 0 or self.dash_timer > 0:
            return False
        self.dash_cd = self.dash_cd_max
        self.dash_timer = 0.18
        self.dash_dir = (math.cos(self.aim_angle), math.sin(self.aim_angle))
        self.invulnerable = True
        return True

    def update_perk_timers(self, dt):
        if self.dash_cd > 0:
            self.dash_cd -= dt

    def draw(self, surf):
        bob_offset = math.sin(self.bob) * 2 if self.bob else 0
        cx, cy = int(self.x), int(self.y + bob_offset)

        shadow = pygame.Surface((self.radius * 3, self.radius), pygame.SRCALPHA)
        pygame.draw.ellipse(shadow, (0, 0, 0, 90), shadow.get_rect())
        surf.blit(shadow, (self.x - self.radius * 1.5, self.y + self.radius - 2))

        if self.player_class == "melee":
            ax = cx + math.cos(self.aim_angle) * self.attack_range
            ay = cy + math.sin(self.aim_angle) * self.attack_range
        else:
            ax = cx + math.cos(self.aim_angle) * 200
            ay = cy + math.sin(self.aim_angle) * 200
        aim = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
        pygame.draw.line(aim, (255, 255, 255, 40), (cx, cy), (ax, ay), 1)
        surf.blit(aim, (0, 0))

        # Цвет скина
        if self.skin.get("rainbow"):
            t = pygame.time.get_ticks() / 1000.0
            base = (int(128 + 127 * math.sin(t)),
                    int(128 + 127 * math.sin(t + 2.1)),
                    int(128 + 127 * math.sin(t + 4.2)))
            glow_color = tuple(min(255, c + 40) for c in base)
        else:
            base = self.skin["color"]
            glow_color = self.skin["glow"]

        if self.hit_flash > 0:
            glow_color = (255, 120, 120)
        if self.invulnerable:
            glow_color = (255, 255, 255)

        draw_glow_circle(surf, base, (cx, cy), self.radius, glow_color)
        bright = tuple(min(255, c + 60) for c in base)
        pygame.draw.circle(surf, bright,
                           (cx - self.radius // 3, cy - self.radius // 3),
                           self.radius // 3)
        pygame.draw.circle(surf, (240, 240, 255), (cx, cy), self.radius, 2)

        if self.player_class == "melee":
            sx = cx + math.cos(self.aim_angle) * (self.radius + 4)
            sy = cy + math.sin(self.aim_angle) * (self.radius + 4)
            ex = cx + math.cos(self.aim_angle) * (self.radius + 22)
            ey = cy + math.sin(self.aim_angle) * (self.radius + 22)
            pygame.draw.line(surf, (230, 230, 240), (sx, sy), (ex, ey), 4)
            pygame.draw.line(surf, (255, 255, 255), (sx, sy), (ex, ey), 2)
        else:
            sx = cx + math.cos(self.aim_angle) * (self.radius + 2)
            sy = cy + math.sin(self.aim_angle) * (self.radius + 2)
            ex = cx + math.cos(self.aim_angle) * (self.radius + 18)
            ey = cy + math.sin(self.aim_angle) * (self.radius + 18)
            pygame.draw.line(surf, (200, 200, 210), (sx, sy), (ex, ey), 6)
            pygame.draw.circle(surf, (255, 230, 150), (int(ex), int(ey)), 4)

        if self.hit_flash > 0:
            self.hit_flash -= 1 / 60


# ---------- Враги ----------
class Zombie:
    def __init__(self, x, y, speed, hp=1):
        self.x, self.y = x, y
        self.radius = ZOMBIE_RADIUS
        self.hp = self.max_hp = hp
        self.speed = speed
        self.damage = 1
        self.hit_timer = 0.0
        self.attack_cd = 0.0
        self.spawn = 0.5
        self.angle = 0.0
        self.wobble = random.uniform(0, math.tau)
        self.knock_vx = 0.0
        self.knock_vy = 0.0
        self.is_phantom = False
        self.is_fat = False
        self.is_summoner = False
        self.is_tank = False
        self.is_splitter = False
        self.is_child = False

    def update(self, player, dt):
        self.x += self.knock_vx * dt
        self.y += self.knock_vy * dt
        self.knock_vx *= 0.85
        self.knock_vy *= 0.85
        if abs(self.knock_vx) < 1: self.knock_vx = 0
        if abs(self.knock_vy) < 1: self.knock_vy = 0
        if self.spawn > 0:
            self.spawn -= dt
            return
        dx = player.x - self.x
        dy = player.y - self.y
        d = math.hypot(dx, dy)
        if d > 0:
            self.angle = math.atan2(dy, dx)
            self.x += dx / d * self.speed * dt * 60
            self.y += dy / d * self.speed * dt * 60
        if self.hit_timer > 0: self.hit_timer -= dt
        if self.attack_cd > 0: self.attack_cd -= dt
        self.wobble += dt * 8

    def draw(self, surf):
        r = self.radius
        if self.spawn > 0:
            r = max(1, int(self.radius * (1 - self.spawn / 0.5)))
        cx, cy = int(self.x), int(self.y)
        shadow = pygame.Surface((r * 3, r), pygame.SRCALPHA)
        pygame.draw.ellipse(shadow, (0, 0, 0, 80), shadow.get_rect())
        surf.blit(shadow, (self.x - r * 1.5, self.y + r - 2))
        base = (200, 60, 60) if self.hit_timer <= 0 else (255, 220, 220)
        dark = (110, 20, 20)
        pulse = math.sin(self.wobble) * 1.5
        pygame.draw.circle(surf, dark, (cx, cy), int(r + 2 + pulse))
        pygame.draw.circle(surf, base, (cx, cy), int(r + pulse))
        pygame.draw.circle(surf, (255, 160, 160), (cx - r // 3, cy - r // 3), max(2, r // 4))
        ex = math.cos(self.angle) * 3
        ey = math.sin(self.angle) * 3
        pygame.draw.circle(surf, (255, 240, 100), (int(cx + ex - 3), int(cy + ey - 1)), 2)
        pygame.draw.circle(surf, (255, 240, 100), (int(cx + ex + 3), int(cy + ey - 1)), 2)


class Phantom(Zombie):
    def __init__(self, x, y, speed):
        super().__init__(x, y, speed, hp=1)
        self.radius = PHANTOM_RADIUS
        self.is_phantom = True

    def draw(self, surf):
        r = self.radius
        if self.spawn > 0:
            r = max(1, int(self.radius * (1 - self.spawn / 0.5)))
        cx, cy = int(self.x), int(self.y)
        shadow = pygame.Surface((r * 3, r), pygame.SRCALPHA)
        pygame.draw.ellipse(shadow, (0, 0, 0, 60), shadow.get_rect())
        surf.blit(shadow, (self.x - r * 1.5, self.y + r - 2))
        flick = 0.75 + 0.25 * math.sin(self.wobble * 2.5)
        alpha = int(200 * flick)
        aura = pygame.Surface((r * 6, r * 6), pygame.SRCALPHA)
        for i in range(5, 0, -1):
            a = int(40 * (i / 5) * flick)
            pygame.draw.circle(aura, (200, 60, 220, a), (r * 3, r * 3), r + i * 3)
        surf.blit(aura, (cx - r * 3, cy - r * 3))
        body = pygame.Surface((r * 2 + 6, r * 2 + 6), pygame.SRCALPHA)
        col_outer = (170, 30, 130, alpha) if self.hit_timer <= 0 else (255, 220, 255, alpha)
        col_inner = (230, 80, 120, alpha)
        pygame.draw.circle(body, col_outer, (r + 3, r + 3), r + 2)
        pygame.draw.circle(body, col_inner, (r + 3, r + 3), r)
        pygame.draw.circle(body, (255, 180, 255, alpha // 2), (r + 3, r + 1), max(2, r // 3))
        surf.blit(body, (cx - r - 3, cy - r - 3))
        ex = math.cos(self.angle) * 2
        ey = math.sin(self.angle) * 2
        pygame.draw.circle(surf, (255, 60, 60), (int(cx + ex - 2), int(cy + ey - 1)), 2)
        pygame.draw.circle(surf, (255, 60, 60), (int(cx + ex + 2), int(cy + ey - 1)), 2)


class FatZombie(Zombie):
    def __init__(self, x, y, speed, hp):
        super().__init__(x, y, speed, hp=hp)
        self.radius = FAT_RADIUS
        self.is_fat = True

    def draw(self, surf):
        r = self.radius
        if self.spawn > 0:
            r = max(1, int(self.radius * (1 - self.spawn / 0.5)))
        cx, cy = int(self.x), int(self.y)
        shadow = pygame.Surface((r * 3, r), pygame.SRCALPHA)
        pygame.draw.ellipse(shadow, (0, 0, 0, 90), shadow.get_rect())
        surf.blit(shadow, (self.x - r * 1.5, self.y + r - 2))
        base = (90, 190, 90) if self.hit_timer <= 0 else (220, 255, 220)
        dark = (40, 110, 40)
        pulse = math.sin(self.wobble) * 2.0
        pygame.draw.circle(surf, dark, (cx, cy), int(r + 3 + pulse))
        pygame.draw.circle(surf, base, (cx, cy), int(r + pulse))
        for ang, dist in [(0.5, 0.45), (2.4, 0.5), (4.2, 0.4)]:
            bx = cx + math.cos(ang) * r * dist
            by = cy + math.sin(ang) * r * dist