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


import pygame
import random
import math
import sys
import time

pygame.init()
pygame.mixer.init()

# =====================================================================
# КОНФИГУРАЦИЯ И КОНСТАНТЫ
# =====================================================================
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 760
WORLD_WIDTH = 4800
WORLD_HEIGHT = 3800
FPS = 60

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("The Witcher 2D: Chronicles of Velen & Oxenfurt")
clock = pygame.time.Clock()

# Шрифты
FONT_TINY = pygame.font.SysFont("georgia", 11)
FONT_HUD = pygame.font.SysFont("consolas", 12, bold=True)
FONT_UI = pygame.font.SysFont("georgia", 14, bold=True)
FONT_DIALOG = pygame.font.SysFont("georgia", 16)
FONT_DIALOG_BOLD = pygame.font.SysFont("georgia", 17, bold=True)
FONT_HEADER = pygame.font.SysFont("georgia", 22, bold=True)
FONT_TITLE = pygame.font.SysFont("georgia", 32, bold=True)

# Палитра
COLOR_BG_NORMAL = (34, 44, 32)
COLOR_BG_SENSE = (18, 18, 18)
COLOR_GOLD = (245, 200, 60)
COLOR_SILVER = (210, 230, 255)
COLOR_STEEL = (220, 200, 160)
COLOR_HP = (220, 35, 35)
COLOR_STAMINA = (245, 185, 25)
COLOR_TOXICITY = (40, 215, 80)
COLOR_UI_BG = (22, 19, 17)
COLOR_UI_BORDER = (150, 125, 75)

# =====================================================================
# МАТЕМАТИКА И ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# =====================================================================
def clamp(val, min_v, max_v):
    return max(min_v, min(val, max_v))

def distance_pts(p1, p2):
    return math.hypot(p1[0] - p2[0], p1[1] - p2[1])

def angle_to_vector(ang):
    return pygame.Vector2(math.cos(ang), math.sin(ang))

# =====================================================================
# КАМЕРА С ОРБИТАЛЬНОЙ ПЛАВНОСТЬЮ И ТРЯСКОЙ
# =====================================================================
class Camera:
    def __init__(self):
        self.x = 0.0
        self.y = 0.0
        self.shake_power = 0.0

    def update(self, target_pos):
        tx = target_pos[0] - SCREEN_WIDTH // 2
        ty = target_pos[1] - SCREEN_HEIGHT // 2
        self.x += (tx - self.x) * 0.12
        self.y += (ty - self.y) * 0.12
        self.x = clamp(self.x, 0, WORLD_WIDTH - SCREEN_WIDTH)
        self.y = clamp(self.y, 0, WORLD_HEIGHT - SCREEN_HEIGHT)

        if self.shake_power > 0:
            self.x += random.uniform(-self.shake_power, self.shake_power)
            self.y += random.uniform(-self.shake_power, self.shake_power)
            self.shake_power = max(0.0, self.shake_power - 0.7)

    def apply_rect(self, rect):
        return rect.move(-int(self.x), -int(self.y))

    def apply_pos(self, pos):
        return (int(pos[0] - self.x), int(pos[1] - self.y))

    def screen_to_world(self, screen_pos):
        return (screen_pos[0] + int(self.x), screen_pos[1] + int(self.y))

camera = Camera()

# =====================================================================
# СИСТЕМА ЧАСТИЦ, СЛЕДОВ И ДЕКОРАЦИЙ
# =====================================================================
class Particle:
    def __init__(self, x, y, vx, vy, color, radius, decay, p_type="circle"):
        self.x = x
        self.y = y
        self.vx = vx
        self.vy = vy
        self.color = color
        self.radius = radius
        self.decay = decay
        self.p_type = p_type

    def update(self):
        self.x += self.vx
        self.y += self.vy
        self.radius -= self.decay

    def draw(self, surf):
        if self.radius <= 0:
            return
        sp = camera.apply_pos((self.x, self.y))
        if self.p_type == "circle":
            pygame.draw.circle(surf, self.color, sp, int(self.radius))
        elif self.p_type == "glow":
            s = pygame.Surface((int(self.radius * 4), int(self.radius * 4)), pygame.SRCALPHA)
            pygame.draw.circle(s, (*self.color[:3], 90), (int(self.radius * 2), int(self.radius * 2)), int(self.radius * 2))
            surf.blit(s, (sp[0] - int(self.radius * 2), sp[1] - int(self.radius * 2)))

class FloatingText:
    def __init__(self, x, y, text, color, size=16, duration=50):
        self.x = x
        self.y = y
        self.text = text
        self.color = color
        self.life = duration
        self.font = pygame.font.SysFont("georgia", size, bold=True)

    def update(self):
        self.y -= 0.85
        self.life -= 1

    def draw(self, surf):
        r = self.font.render(self.text, True, self.color)
        surf.blit(r, camera.apply_pos((self.x - r.get_width() // 2, self.y)))

class BloodDecal:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.radius = random.randint(8, 18)
        self.color = (random.randint(90, 130), 12, 16)

    def draw(self, surf):
        sp = camera.apply_pos((self.x, self.y))
        pygame.draw.circle(surf, self.color, sp, self.radius)

class Footprint:
    def __init__(self, x, y, angle, is_monster=False):
        self.x = x
        self.y = y
        self.angle = angle
        self.is_monster = is_monster

    def draw(self, surf, sense_active):
        if not sense_active and self.is_monster:
            return
        sp = camera.apply_pos((self.x, self.y))
        col = (255, 45, 45) if self.is_monster else (30, 25, 20)
        s = pygame.Surface((12, 18), pygame.SRCALPHA)
        if self.is_monster:
            pygame.draw.circle(s, col, (6, 9), 5)
            pygame.draw.line(s, col, (2, 2), (10, 4), 2)
            pygame.draw.line(s, col, (1, 8), (11, 8), 2)
        else:
            pygame.draw.ellipse(s, col, (2, 2, 8, 14))
        rot = pygame.transform.rotate(s, -math.degrees(self.angle))
        surf.blit(rot, (sp[0] - rot.get_width() // 2, sp[1] - rot.get_height() // 2))

# =====================================================================
# ПРОЦЕДУРНЫЕ СПРАЙТЫ (ДЕТАЛИЗИРОВАННЫЕ МОДЕЛИ)
# =====================================================================
def render_witcher_sprite(step, facing, attacking, sword_type, combo_step):
    surf = pygame.Surface((56, 62), pygame.SRCALPHA)
    leg = int(math.sin(step * 0.28) * 6)
    cx, cy = 28, 30

    # Сапоги и штаны
    pygame.draw.rect(surf, (22, 18, 14), (17, 42 + leg, 8, 15))
    pygame.draw.rect(surf, (22, 18, 14), (31, 42 - leg, 8, 15))

    # Доспех Школы Волка (куртка, заклепки, портупея)
    pygame.draw.rect(surf, (46, 40, 32), (14, 18, 28, 26), border_radius=4)
    pygame.draw.rect(surf, (85, 48, 32), (18, 20, 20, 15))
    pygame.draw.circle(surf, (190, 190, 200), (cx, 24), 3) # Медальон Волка
    pygame.draw.line(surf, (120, 70, 40), (14, 20), (42, 38), 2) # Ремень портупеи

    # Два меча за спиной (ножны крестом)
    pygame.draw.line(surf, (140, 140, 150), (10, 10), (38, 48), 3) # Стальной
    pygame.draw.line(surf, (215, 235, 255), (46, 10), (18, 48), 3) # Серебряный

    # Голова и седые волосы Геральта
    pygame.draw.circle(surf, (220, 190, 160), (cx, 13), 10) # Лицо
    pygame.draw.arc(surf, (245, 245, 250), (16, 2, 24, 20), 0, 3.14, 5) # Волосы
    pygame.draw.circle(surf, (245, 245, 250), (cx, 6), 8)
    pygame.draw.rect(surf, (245, 245, 250), (16, 11, 4, 10)) # Пряди
    pygame.draw.rect(surf, (245, 245, 250), (36, 11, 4, 10))

    # Кошачьи ведьмачьи глаза
    dx = int(facing.x * 3)
    dy = int(facing.y * 2)
    pygame.draw.circle(surf, (255, 195, 0), (cx - 4 + dx, 13 + dy), 2)
    pygame.draw.circle(surf, (255, 195, 0), (cx + 4 + dx, 13 + dy), 2)

    # Атакующий клинок в руках
    if attacking:
        col = (215, 240, 255) if sword_type == "SILVER" else (235, 210, 170)
        hand_x = cx + int(facing.x * 12)
        hand_y = cy + int(facing.y * 12)
        tip_x = hand_x + int(facing.x * 30)
        tip_y = hand_y + int(facing.y * 30)
        pygame.draw.line(surf, col, (hand_x, hand_y), (tip_x, tip_y), 4)
        pygame.draw.circle(surf, (255, 255, 255), (tip_x, tip_y), 3)

    return surf

def render_npc_sprite(role="elder"):
    surf = pygame.Surface((48, 56), pygame.SRCALPHA)
    cx = 24
    pygame.draw.rect(surf, (45, 35, 25), (15, 38, 8, 16))
    pygame.draw.rect(surf, (45, 35, 25), (25, 38, 8, 16))

    if role == "elder":
        # Староста: славянская рубаха с орнаментом и седая борода
        pygame.draw.rect(surf, (190, 180, 160), (13, 16, 22, 24), border_radius=3)
        pygame.draw.rect(surf, (160, 40, 40), (13, 26, 22, 4)) # Красный пояс
        pygame.draw.circle(surf, (220, 185, 155), (cx, 13), 9)
        pygame.draw.ellipse(surf, (200, 195, 190), (16, 12, 16, 14)) # Борода
    elif role == "herbalist":
        # Травница: зеленый сарафан, рыжие волосы
        pygame.draw.rect(surf, (45, 115, 75), (13, 16, 22, 25), border_radius=4)
        pygame.draw.circle(surf, (230, 195, 165), (cx, 13), 9)
        pygame.draw.arc(surf, (150, 50, 25), (14, 4, 20, 16), 0, 3.14, 5) # Волосы
        pygame.draw.circle(surf, (150, 50, 25), (cx, 7), 8)
    elif role == "baron":
        # Барон: богатый кафтан с меховой опушкой
        pygame.draw.rect(surf, (110, 35, 45), (12, 15, 24, 26), border_radius=4)
        pygame.draw.rect(surf, (200, 175, 70), (12, 15, 24, 6)) # Золотая цепь
        pygame.draw.circle(surf, (220, 185, 155), (cx, 13), 10)
        pygame.draw.ellipse(surf, (60, 45, 35), (16, 14, 16, 10)) # Темные усы
    return surf

def render_house_surface(w, h):
    surf = pygame.Surface((w, h), pygame.SRCALPHA)
    # Стены сруба
    pygame.draw.rect(surf, (72, 48, 30), (12, 45, w - 24, h - 45))
    for y in range(45, h, 14):
        pygame.draw.line(surf, (48, 32, 20), (12, y), (w - 12, y), 2)
    # Дверь и окна
    pygame.draw.rect(surf, (40, 25, 15), (w // 2 - 16, h - 42, 32, 42))
    pygame.draw.rect(surf, (180, 210, 230), (25, 60, 22, 22))
    pygame.draw.rect(surf, (50, 35, 20), (25, 60, 22, 22), 2)
    pygame.draw.rect(surf, (180, 210, 230), (w - 47, 60, 22, 22))
    pygame.draw.rect(surf, (50, 35, 20), (w - 47, 60, 22, 22), 2)
    # Соломенная крыша
    pygame.draw.polygon(surf, (140, 115, 60), [(0, 45), (w // 2, 0), (w, 45)])
    pygame.draw.polygon(surf, (160, 135, 75), [(8, 45), (w // 2, 7), (w - 8, 45)], 2)
    return surf

# =====================================================================
# ИГРОК: ВЕДЬМАК ГЕРАЛЬТ
# =====================================================================
class Witcher:
    def __init__(self, x, y):
        self.rect = pygame.Rect(x, y, 34, 46)
        self.hp = 220
        self.max_hp = 220
        self.stamina = 100
        self.max_stamina = 100
        self.toxicity = 0
        self.coins = 140
        self.potions = 3
        self.bombs = 2
        self.herbs = 5

        self.speed = 4.4
        self.facing = pygame.Vector2(0, 1)
        self.step = 0

        self.sword = "SILVER" # "SILVER" или "STEEL"
        self.combo = 0
        self.attacking = False
        self.atk_timer = 0
        self.attack_hitbox = pygame.Rect(0, 0, 0, 0)
        self.dodge_timer = 0
        self.quen = False
        self.sense = False

        self.inventory = [
            {"id": "silv_sword", "name": "Серебряный клинок Волка", "type": "weapon", "desc": "Базовый урон +45 по чудовищам."},
            {"id": "steel_sword", "name": "Темерский стальной меч", "type": "weapon", "desc": "Базовый урон +40 по людям и зверям."},
            {"id": "potion_swallow", "name": "Эликсир Ласточка", "type": "potion", "desc": "Мгновенно восстанавливает 60 HP."},
            {"id": "bomb_samum", "name": "Бомба Самум", "type": "bomb", "desc": "Ослепляет и оглушает врагов в радиусе."},
            {"id": "herb_celandine", "name": "Ласточкина трава", "type": "herb", "desc": "Ингредиент алхимии для настоек."}
        ]

    def update(self, obstacles):
        if self.stamina < self.max_stamina:
            self.stamina = min(self.max_stamina, self.stamina + 0.42)

        if self.toxicity > 0:
            self.toxicity = max(0, self.toxicity - 0.035)

        if self.dodge_timer > 0:
            self.dodge_timer -= 1

        if self.atk_timer > 0:
            self.atk_timer -= 1
            if self.atk_timer == 0:
                self.attacking = False
                self.attack_hitbox = pygame.Rect(0, 0, 0, 0)

    def move(self, move_vec, obstacles, footprints):
        spd = self.speed * 2.1 if self.dodge_timer > 0 else (self.speed * 0.6 if self.sense else self.speed)
        if move_vec.length() > 0:
            self.step += 1
            if self.step % 18 == 0:
                ang = math.atan2(self.facing.y, self.facing.x)
                footprints.append(Footprint(self.rect.centerx, self.rect.bottom, ang, False))

            # Перемещение X с коллизией
            self.rect.x += int(move_vec.x * spd)
            for obs in obstacles:
                if self.rect.colliderect(obs):
                    if move_vec.x > 0: self.rect.right = obs.left
                    if move_vec.x < 0: self.rect.left = obs.right

            # Перемещение Y с коллизией
            self.rect.y += int(move_vec.y * spd)
            for obs in obstacles:
                if self.rect.colliderect(obs):
                    if move_vec.y > 0: self.rect.bottom = obs.top
                    if move_vec.y < 0: self.rect.top = obs.bottom
        else:
            self.step = 0

        self.rect.clamp_ip(pygame.Rect(0, 0, WORLD_WIDTH, WORLD_HEIGHT))

    def attack_target(self, target_world_pos):
        if not self.attacking and self.stamina >= 12:
            self.attacking = True
            self.stamina -= 12
            self.atk_timer = 16
            self.combo = (self.combo % 3) + 1

            dir_vec = pygame.Vector2(target_world_pos) - pygame.Vector2(self.rect.center)
            if dir_vec.length() > 0:
                self.facing = dir_vec.normalize()

            reach = 46
            ax = self.rect.centerx + int(self.facing.x * reach) - 30
            ay = self.rect.centery + int(self.facing.y * reach) - 30
            self.attack_hitbox = pygame.Rect(ax, ay, 60, 60)

    def drink_potion(self, texts):
        if self.potions > 0 and self.toxicity <= 80:
            self.potions -= 1
            self.hp = min(self.max_hp, self.hp + 65)
            self.toxicity += 30
            texts.append(FloatingText(self.rect.centerx, self.rect.top, "+65 HP (Ласточка)", (60, 255, 120)))
        elif self.toxicity > 80:
            texts.append(FloatingText(self.rect.centerx, self.rect.top, "Интоксикация высока!", (255, 50, 50)))

    def throw_bomb(self, target_world_pos, particles, texts, enemies, boss):
        if self.bombs > 0 and self.stamina >= 20:
            self.bombs -= 1
            self.stamina -= 20
            tx, ty = target_world_pos
            camera.shake_power = 9.0
            texts.append(FloatingText(tx, ty, "ВЗРЫВ САМУМА!", COLOR_GOLD, 20))
            for _ in range(50):
                ang = random.uniform(0, 6.28)
                v = angle_to_vector(ang) * random.uniform(4, 14)
                particles.append(Particle(tx, ty, v.x, v.y, (255, 230, 100), random.randint(5, 12), 0.35, "glow"))

            for e in enemies:
                if distance_pts((tx, ty), e.rect.center) < 140:
                    e.hp -= 40
                    e.stun_timer = 120
                    texts.append(FloatingText(e.rect.centerx, e.rect.top, "-40 (Оглушен)", COLOR_GOLD))
            if boss.alive and distance_pts((tx, ty), boss.rect.center) < 170:
                boss.hp -= 45
                boss.stun_timer = 90
                texts.append(FloatingText(boss.rect.centerx, boss.rect.top, "-45 (Оглушен)", COLOR_GOLD))

    def draw(self, surf):
        r = camera.apply_rect(self.rect)
        pygame.draw.ellipse(surf, (15, 20, 15, 130), (r.centerx - 17, r.bottom - 7, 34, 14))
        sprite = render_witcher_sprite(self.step, self.facing, self.attacking, self.sword, self.combo)
        surf.blit(sprite, (r.centerx - 28, r.centery - 31))

        if self.quen:
            pygame.draw.circle(surf, (255, 220, 30), r.center, 36, 2)
            qs = pygame.Surface((72, 72), pygame.SRCALPHA)
            pygame.draw.circle(qs, (255, 220, 30, 45), (36, 36), 36)
            surf.blit(qs, (r.centerx - 36, r.centery - 36))

# =====================================================================
# ВРАГИ: УТОПЦЫ, РАЗБОЙНИКИ, ДРЕВНИЙ ЛЕШИЙ
# =====================================================================
class Enemy:
    def __init__(self, x, y, e_type):
        self.rect = pygame.Rect(x, y, 34, 44)
        self.e_type = e_type # "DROWNER", "BANDIT"
        self.is_monster = (e_type == "DROWNER")
        self.hp = 80 if self.is_monster else 100
        self.max_hp = self.hp
        self.speed = 2.8 if self.is_monster else 2.5
        self.hit_cd = 0
        self.slow_timer = 0
        self.stun_timer = 0

    def update(self, player, texts, decals):
        if self.stun_timer > 0:
            self.stun_timer -= 1
            return
        if self.slow_timer > 0:
            self.slow_timer -= 1

        spd = self.speed * 0.35 if self.slow_timer > 0 else self.speed
        self.hit_cd = max(0, self.hit_cd - 1)

        v = pygame.Vector2(player.rect.center) - pygame.Vector2(self.rect.center)
        dist = v.length()

        if 20 < dist < 520:
            d = v.normalize()
            self.rect.x += int(d.x * spd)
            self.rect.y += int(d.y * spd)

        if self.rect.colliderect(player.rect) and self.hit_cd == 0 and player.dodge_timer == 0:
            self.hit_cd = 45
            if player.quen:
                player.quen = False
                texts.append(FloatingText(player.rect.centerx, player.rect.top, "КВЕН СПАС!", COLOR_GOLD))
            else:
                dmg = random.randint(18, 28)
                player.hp -= dmg
                camera.shake_power = 6.0
                texts.append(FloatingText(player.rect.centerx, player.rect.top, f"-{dmg}", COLOR_HP))
                decals.append(BloodDecal(player.rect.centerx, player.rect.centery))

    def draw(self, surf, sense_active):
        r = camera.apply_rect(self.rect)
        if self.e_type == "DROWNER":
            pygame.draw.rect(surf, (40, 120, 125), r, border_radius=4)
            pygame.draw.circle(surf, (255, 45, 45), (r.centerx - 4, r.y + 8), 2)
            pygame.draw.circle(surf, (255, 45, 45), (r.centerx + 4, r.y + 8), 2)
        else:
            pygame.draw.rect(surf, (95, 60, 45), r, border_radius=3)
            pygame.draw.circle(surf, (220, 185, 145), (r.centerx, r.y + 8), 6)
            pygame.draw.line(surf, (185, 185, 185), (r.right, r.centery), (r.right + 10, r.centery - 6), 3)

        if self.stun_timer > 0:
            pygame.draw.circle(surf, (140, 220, 255), (r.centerx, r.top - 8), 5, 2)

        if sense_active:
            col = (255, 80, 0, 160) if self.is_monster else (230, 220, 50, 160)
            glow = pygame.Surface((r.w + 14, r.h + 14), pygame.SRCALPHA)
            pygame.draw.rect(glow, col, glow.get_rect(), width=3, border_radius=6)
            surf.blit(glow, (r.x - 7, r.y - 7))

        pct = max(0, self.hp / self.max_hp)
        pygame.draw.rect(surf, (20, 20, 20), (r.x, r.y - 8, self.rect.w, 4))
        pygame.draw.rect(surf, COLOR_HP, (r.x, r.y - 8, int(self.rect.w * pct), 4))

class AncientLeshenBoss:
    def __init__(self, x, y):
        self.rect = pygame.Rect(x, y, 80, 110)
        self.max_hp = 850
        self.hp = 850
        self.alive = True
        self.teleport_cd = 220
        self.root_cd = 140
        self.slow_timer = 0
        self.stun_timer = 0

    def update(self, player, roots, texts, particles):
        if not self.alive:
            return
        if self.stun_timer > 0:
            self.stun_timer -= 1
            return
        if self.slow_timer > 0:
            self.slow_timer -= 1

        self.teleport_cd -= 1
        self.root_cd -= 1

        v = pygame.Vector2(player.rect.center) - pygame.Vector2(self.rect.center)
        dist = v.length()

        # Телепортация в вороньем тумане
        if self.teleport_cd <= 0 and dist < 800:
            self.teleport_cd = 250
            for _ in range(45):
                particles.append(Particle(self.rect.centerx, self.rect.centery, random.uniform(-5, 5), random.uniform(-5, 5), (35, 35, 35), random.randint(6, 14), 0.22, "glow"))
            ang = random.uniform(0, 6.28)
            self.rect.x = int(player.rect.centerx + math.cos(ang) * 230)
            self.rect.y = int(player.rect.centery + math.sin(ang) * 230)
            texts.append(FloatingText(self.rect.centerx, self.rect.top, "*Шелест вековых ветвей*", (190, 230, 190), 16))

        # Призыв корней
        if self.root_cd <= 0 and dist < 700:
            self.root_cd = 160
            roots.append(RootTrap(player.rect.centerx, player.rect.centery))
            texts.append(FloatingText(self.rect.centerx, self.rect.top, "ГНЕВ ЧАЩИ!", (140, 210, 90), 18))

        spd = 1.7 if self.slow_timer == 0 else 0.75
        if dist > 45:
            d = v.normalize()
            self.rect.x += int(d.x * spd)
            self.rect.y += int(d.y * spd)

    def draw(self, surf, sense_active):
        if not self.alive:
            return
        r = camera.apply_rect(self.rect)
        pygame.draw.ellipse(surf, (0, 0, 0, 140), (r.centerx - 32, r.bottom - 12, 64, 24))
        pygame.draw.rect(surf, (42, 28, 20), r, border_radius=6)

        # Олений череп и рога
        pygame.draw.polygon(surf, (225, 220, 195), [(r.centerx, r.top + 36), (r.left + 14, r.top + 8), (r.right - 14, r.top + 8)])
        pygame.draw.line(surf, (30, 20, 15), (r.left + 16, r.top + 8), (r.left - 20, r.top - 26), 4)
        pygame.draw.line(surf, (30, 20, 15), (r.right - 16, r.top + 8), (r.right + 20, r.top - 26), 4)
        pygame.draw.circle(surf, (180, 255, 215), (r.centerx - 8, r.top + 18), 3)
        pygame.draw.circle(surf, (180, 255, 215), (r.centerx + 8, r.top + 18), 3)

        if sense_active:
            glow = pygame.Surface((r.w + 26, r.h + 26), pygame.SRCALPHA)
            pygame.draw.rect(glow, (255, 35, 0, 180), glow.get_rect(), width=4, border_radius=8)
            surf.blit(glow, (r.x - 13, r.y - 13))

class RootTrap:
    def __init__(self, x, y):
        self.rect = pygame.Rect(x - 36, y - 36, 72, 72)
        self.timer = 42
        self.active_time = 36
        self.hit = False

    def update(self, player, texts, decals):
        self.timer -= 1
        if self.timer <= 0:
            self.active_time -= 1
            if not self.hit and self.rect.colliderect(player.rect):
                self.hit = True
                if player.quen:
                    player.quen = False
                    texts.append(FloatingText(player.rect.centerx, player.rect.top, "КВЕН РАЗБИТ", COLOR_GOLD))
                else:
                    dmg = random.randint(34, 48)
                    player.hp -= dmg
                    camera.shake_power = 11.0
                    texts.append(FloatingText(player.rect.centerx, player.rect.top, f"-{dmg} КОРНИ", COLOR_HP, 18))
                    decals.append(BloodDecal(player.rect.centerx, player.rect.centery))

    def draw(self, surf):
        r = camera.apply_rect(self.rect)
        if self.timer > 0:
            pygame.draw.circle(surf, (145, 65, 45), r.center, int(32 * (1 - self.timer / 42)), 2)
        else:
            pygame.draw.circle(surf, (45, 65, 30), r.center, 34)
            for a in [0, 1.2, 2.4, 3.6, 4.8]:
                ex = r.centerx + int(math.cos(a) * 32)
                ey = r.centery + int(math.sin(a) * 32)
                pygame.draw.line(surf, (65, 38, 22), r.center, (ex, ey), 4)

class YrdenTrap:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.radius = 135
        self.life = 380

    def update(self, enemies, boss):
        self.life -= 1
        for e in enemies:
            if distance_pts((self.x, self.y), e.rect.center) < self.radius:
                e.slow_timer = 25
        if boss.alive and distance_pts((self.x, self.y), boss.rect.center) < self.radius:
            boss.slow_timer = 25

    def draw(self, surf):
        pos = camera.apply_pos((self.x, self.y))
        s = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
        pygame.draw.circle(s, (185, 55, 245, 50), (self.radius, self.radius), self.radius)
        pygame.draw.circle(s, (225, 125, 255, 140), (self.radius, self.radius), self.radius, 2)
        surf.blit(s, (pos[0] - self.radius, pos[1] - self.radius))

# =====================================================================
# ИНТЕРАКТИВНЫЕ ОБЪЕКТЫ И NPC
# =====================================================================
class NPC:
    def __init__(self, x, y, name, role):
        self.rect = pygame.Rect(x, y, 34, 46)
        self.name = name
        self.role = role
        self.sprite = render_npc_sprite(role)

    def draw(self, surf):
        r = camera.apply_rect(self.rect)
        surf.blit(self.sprite, (r.x - 7, r.y - 5))
        pygame.draw.circle(surf, COLOR_GOLD, (r.centerx, r.top - 14), 7)
        surf.blit(FONT_HUD.render("!", True, (10, 10, 10)), (r.centerx - 3, r.top - 21))

class WorldObject:
    def __init__(self, x, y, obj_type):
        self.rect = pygame.Rect(x, y, 30, 30)
        self.obj_type = obj_type # "HERB", "CHEST", "CAMPFIRE"
        self.looted = False

    def draw(self, surf, sense_active):
        if self.looted:
            return
        r = camera.apply_rect(self.rect)
        if self.obj_type == "HERB":
            pygame.draw.circle(surf, (55, 210, 85), r.center, 8)
            pygame.draw.circle(surf, (240, 255, 110), (r.centerx, r.centery - 2), 4)
        elif self.obj_type == "CHEST":
            pygame.draw.rect(surf, (115, 75, 35), r, border_radius=3)
            pygame.draw.rect(surf, COLOR_GOLD, (r.x + 2, r.y + 11, r.w - 4, 4))
        elif self.obj_type == "CAMPFIRE":
            pygame.draw.circle(surf, (40, 35, 30), r.center, 16)
            pygame.draw.circle(surf, (255, 120, 20), r.center, 8)

        if sense_active and self.obj_type != "CAMPFIRE":
            glow = pygame.Surface((r.w + 12, r.h + 12), pygame.SRCALPHA)
            col = (255, 140, 0, 160) if self.obj_type == "CHEST" else (110, 255, 130, 160)
            pygame.draw.rect(glow, col, glow.get_rect(), width=2, border_radius=4)
            surf.blit(glow, (r.x - 6, r.y - 6))

# =====================================================================
# ИНИЦИАЛИЗАЦИЯ ИГРОВОГО МИРА
# =====================================================================
witcher = Witcher(750, 750)
elder = NPC(850, 720, "Староста Богдан", "elder")
herbalist = NPC(620, 920, "Травница Томира", "herbalist")
baron = NPC(1100, 680, "Барон Врониц", "baron")
leshen = AncientLeshenBoss(3800, 2400)

houses = [
    (pygame.Rect(550, 520, 180, 140), render_house_surface(180, 140)),
    (pygame.Rect(900, 500, 200, 150), render_house_surface(200, 150)),
    (pygame.Rect(700, 1000, 190, 140), render_house_surface(190, 140))
]
obstacles = [h[0] for h in houses]

# Деревья и древние монолиты
for _ in range(95):
    obstacles.append(pygame.Rect(random.randint(1400, WORLD_WIDTH - 250), random.randint(200, WORLD_HEIGHT - 250), 60, 60))
for _ in range(25):
    obstacles.append(pygame.Rect(random.randint(2800, WORLD_WIDTH - 400), random.randint(1500, WORLD_HEIGHT - 400), 100, 80))

# Враги
enemies = []
for _ in range(16): # Утопцы
    enemies.append(Enemy(random.randint(1600, 3100), random.randint(600, 2000), "DROWNER"))
for _ in range(10): # Разбойники
    enemies.append(Enemy(random.randint(1000, 2200), random.randint(2400, 3400), "BANDIT"))

# Лут
world_objects = [WorldObject(780, 700, "CAMPFIRE")]
for _ in range(35):
    world_objects.append(WorldObject(random.randint(300, WORLD_WIDTH - 300), random.randint(300, WORLD_HEIGHT - 300), "HERB"))
for _ in range(14):
    world_objects.append(WorldObject(random.randint(600, WORLD_WIDTH - 500), random.randint(500, WORLD_HEIGHT - 500), "CHEST"))

# Кровавые следы Лешего для Чутья
monster_tracks = []
tr_start = pygame.Vector2(1050, 700)
tr_end = pygame.Vector2(3750, 2380)
for i in range(32):
    p = tr_start.lerp(tr_end, i / 32.0)
    monster_tracks.append(Footprint(p.x + random.randint(-22, 22), p.y + random.randint(-22, 22), 0.65, True))

footprints = []
particles = []
floating_texts = []
blood_decals = []
yrdens = []
roots = []

# =====================================================================
# ДАННЫЕ БЕСТИАРИЯ И КВЕСТОВ
# =====================================================================
quest_log = {
    "leshen": {"name": "Заказ: Древний Хозяин Леса", "stage": 0, "desc": "Староста Залипья молит о помощи: в восточной пуще свирепствует Леший."},
    "herbs": {"name": "Сбор для Травницы", "stage": 0, "desc": "Томира просит принести 3 цветка ласточкиной травы для зелий."},
    "bandits": {"name": "Беспорядки на Тракте", "stage": 0, "desc": "Барон приказал выбить банду дезертиров с южного перекрестка."}
}

bestiary_database = {
    "Утопец": {
        "class": "Трупоед",
        "desc": "Склизкие твари, обитающие близ болот и топей. Опасны в стаях.",
        "weakness": "Серебряный меч, Знак Игни, Самум, Масло против трупоедов."
    },
    "Древний Леший": {
        "class": "Реликт",
        "desc": "Могущественный дух чащи. Управляет корнями, стаями ворон и туманом.",
        "weakness": "Серебряный меч, Знак Игни (крайне уязвим к огню), Знак Ирден."
    },
    "Дезертир / Разбойник": {
        "class": "Человек",
        "desc": "Бывшие ратники, обозленные войной и живущие разбоем.",
        "weakness": "Стальной меч, Знак Аард (сбивание), Знак Аксий (гипноз)."
    }
}

menu_active = False
menu_tab = "INV" # "INV", "BESTIARY", "QUESTS"
dialog_active = None

# =====================================================================
# ИГРОВОЙ ЦИКЛ
# =====================================================================
running = True
while running:
    mouse_buttons = pygame.mouse.get_pressed()
    mouse_x, mouse_y = pygame.mouse.get_pos()
    mouse_world = camera.screen_to_world((mouse_x, mouse_y))
    keys = pygame.key.get_pressed()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        if event.type == pygame.KEYDOWN:
            # Открытие / Закрытие меню
            if event.key == pygame.K_i:
                menu_active = not menu_active

            if menu_active:
                if event.key == pygame.K_1: menu_tab = "INV"
                if event.key == pygame.K_2: menu_tab = "BESTIARY"
                if event.key == pygame.K_3: menu_tab = "QUESTS"
                if event.key == pygame.K_ESCAPE: menu_active = False
                continue

            # Диалоговое окно
            if dialog_active:
                if event.key == pygame.K_1:
                    cb = dialog_active.get("opt1_cb")
                    if cb: cb()
                    dialog_active = None
                elif event.key == pygame.K_2:
                    cb = dialog_active.get("opt2_cb")
                    if cb: cb()
                    dialog_active = None
                elif event.key == pygame.K_ESCAPE:
                    dialog_active = None
                continue

            # Взаимодействие (E)
            if event.key == pygame.K_e:
                # Староста
                if distance_pts(witcher.rect.center, elder.rect.center) < 75:
                    st = quest_log["leshen"]["stage"]
                    if st == 0:
                        def accept_leshen():
                            quest_log["leshen"]["stage"] = 1
                            floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "ЗАКАЗ ПРИНЯТ!", COLOR_GOLD, 20))
                        dialog_active = {
                            "name": elder.name,
                            "text": "Мастер Ведьмак! В восточной пуще объявился Леший. Людей на ветвях развешивает. Помоги!",
                            "opt1": "1. Беру заказ на чудовище. [300 крон]", "opt1_cb": accept_leshen,
                            "opt2": "2. Мне нужно подготовиться.", "opt2_cb": None
                        }
                    elif st == 2:
                        def finish_leshen():
                            quest_log["leshen"]["stage"] = 3
                            witcher.coins += 300
                            floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+300 КРОН", COLOR_GOLD, 24))
                        dialog_active = {
                            "name": elder.name,
                            "text": "Ты сразил чудище! Отныне Залипье твой вечный должник, Белый Волк!",
                            "opt1": "1. Забрать награду (300 крон)", "opt1_cb": finish_leshen,
                            "opt2": "2. Зайду позже.", "opt2_cb": None
                        }
                    else:
                        dialog_active = {"name": elder.name, "text": "Включи свое чутье (ПКМ) и ступай по кровавым следам на восток!", "opt1": "1. Ухожу.", "opt1_cb": None, "opt2": "2. Бывай.", "opt2_cb": None}

                # Травница Томира
                elif distance_pts(witcher.rect.center, herbalist.rect.center) < 75:
                    def buy_swallow():
                        if witcher.coins >= 40:
                            witcher.coins -= 40
                            witcher.potions += 1
                            floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+1 Ласточка", (80, 255, 120)))
                    def give_herbs():
                        if witcher.herbs >= 3:
                            witcher.herbs -= 3
                            witcher.coins += 50
                            quest_log["herbs"]["stage"] = 3
                            floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+50 Крон за травы", COLOR_GOLD))
                    dialog_active = {
                        "name": herbalist.name,
                        "text": "Здравствуй, Геральт. Могу приготовить настойки или купить свежие травы.",
                        "opt1": "1. Купить 'Ласточку' (-40 крон)", "opt1_cb": buy_swallow,
                        "opt2": "2. Сдать 3 травы (+50 крон)", "opt2_cb": give_herbs
                    }

                # Барон
                elif distance_pts(witcher.rect.center, baron.rect.center) < 75:
                    def accept_baron():
                        quest_log["bandits"]["stage"] = 1
                        floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "ЗАДАНИЕ БАРОНА!", COLOR_GOLD))
                    dialog_active = {
                        "name": baron.name,
                        "text": "Ведьмак, дезертиры перекрыли тракт на юге. Вырежи их, и я щедро отсыплю золота.",
                        "opt1": "1. Я разберусь с бандитами.", "opt1_cb": accept_baron,
                        "opt2": "2. Не мое дело.", "opt2_cb": None
                    }

                # Сбор трав и сундуков
                for obj in world_objects:
                    if not obj.looted and witcher.rect.colliderect(obj.rect):
                        obj.looted = True
                        if obj.obj_type == "HERB":
                            witcher.herbs += 1
                            floating_texts.append(FloatingText(obj.rect.centerx, obj.rect.top, "+1 Ласточкина трава", (90, 255, 110)))
                        elif obj.obj_type == "CHEST":
                            c = random.randint(50, 110)
                            witcher.coins += c
                            witcher.bombs += 1
                            floating_texts.append(FloatingText(obj.rect.centerx, obj.rect.top, f"+{c} Крон и Бомба!", COLOR_GOLD, 20))

            # Переключение меча (R)
            if event.key == pygame.K_r:
                witcher.sword = "STEEL" if witcher.sword == "SILVER" else "SILVER"
                name = "СЕРЕБРЯНЫЙ МЕЧ (Чудовища)" if witcher.sword == "SILVER" else "СТАЛЬНОЙ МЕЧ (Люди)"
                col = COLOR_SILVER if witcher.sword == "SILVER" else COLOR_STEEL
                floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, name, col))

            # Использование Ласточки (F)
            if event.key == pygame.K_f:
                witcher.drink_potion(floating_texts)

            # Бросок бомбы (G)
            if event.key == pygame.K_g:
                witcher.throw_bomb(mouse_world, particles, floating_texts, enemies, leshen)

            # Кувырок (LSHIFT)
            if event.key == pygame.K_LSHIFT and witcher.dodge_timer == 0 and witcher.stamina >= 24:
                witcher.dodge_timer = 16
                witcher.stamina -= 24

            # --- 5 ЗНАКОВ ВЕДЬМАКА ---
            if not menu_active and not dialog_active:
                # 1. ИГНИ
                if event.key == pygame.K_1 and witcher.stamina >= 35:
                    witcher.stamina -= 35
                    camera.shake_power = 6.0
                    for _ in range(50):
                        ang = (random.random() - 0.5) * 0.95
                        v = witcher.facing.rotate_rad(ang) * random.uniform(7, 14)
                        particles.append(Particle(witcher.rect.centerx, witcher.rect.centery, v.x, v.y, (255, random.randint(70, 160), 20), random.randint(5, 11), 0.32, "glow"))

                # 2. КВЕН
                if event.key == pygame.K_2 and witcher.stamina >= 40:
                    witcher.stamina -= 40
                    witcher.quen = True
                    floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "КВЕН", COLOR_GOLD))

                # 3. ААРД
                if event.key == pygame.K_3 and witcher.stamina >= 35:
                    witcher.stamina -= 35
                    camera.shake_power = 7.5
                    for _ in range(40):
                        ang = (random.random() - 0.5) * 0.85
                        v = witcher.facing.rotate_rad(ang) * random.uniform(9, 17)
                        particles.append(Particle(witcher.rect.centerx, witcher.rect.centery, v.x, v.y, (190, 225, 255), random.randint(4, 9), 0.3, "glow"))
                    for e in enemies:
                        if distance_pts(witcher.rect.center, e.rect.center) < 220:
                            e.rect.x += int(witcher.facing.x * 90)
                            e.rect.y += int(witcher.facing.y * 90)
                            e.hp -= 22
                            e.stun_timer = 35
                            floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, "ААРД: СТАН", (190, 220, 255)))

                # 4. ИРДЕН
                if event.key == pygame.K_4 and witcher.stamina >= 35:
                    witcher.stamina -= 35
                    yrdens.append(YrdenTrap(witcher.rect.centerx, witcher.rect.centery))
                    floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "ИРДЕН", (210, 110, 255)))

                # 5. АКСИЙ
                if event.key == pygame.K_5 and witcher.stamina >= 35:
                    witcher.stamina -= 35
                    tgt = None
                    for e in enemies:
                        if distance_pts(witcher.rect.center, e.rect.center) < 260:
                            tgt = e
                            break
                    if tgt:
                        tgt.stun_timer = 200
                        floating_texts.append(FloatingText(tgt.rect.centerx, tgt.rect.top, "АКСИЙ: ГИПНОЗ", (100, 255, 210), 18))

        # Атака мечом на ЛКМ
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and not menu_active and not dialog_active:
            witcher.attack_target(mouse_world)

    # Ведьмачье чутье на ПКМ
    witcher.sense = mouse_buttons[2] and not menu_active and not dialog_active

    # Обновление игровой логики
    if not menu_active and not dialog_active:
        mv = pygame.Vector2(0, 0)
        if keys[pygame.K_w]: mv.y -= 1
        if keys[pygame.K_s]: mv.y += 1
        if keys[pygame.K_a]: mv.x -= 1
        if keys[pygame.K_d]: mv.x += 1
        if mv.length() > 0:
            mv = mv.normalize()

        witcher.move(mv, obstacles, footprints)
        witcher.update(obstacles)
        camera.update(witcher.rect.center)

        # Обработка ударов меча
        if witcher.attacking and witcher.attack_hitbox.width > 0:
            # Обычные враги
            for e in enemies:
                if witcher.attack_hitbox.colliderect(e.rect):
                    correct = (e.is_monster and witcher.sword == "SILVER") or (not e.is_monster and witcher.sword == "STEEL")
                    mult = 1.0 if correct else 0.25
                    dmg = int(random.randint(30, 44) * mult)
                    e.hp -= dmg
                    camera.shake_power = 5.0
                    blood_decals.append(BloodDecal(e.rect.centerx, e.rect.centery))
                    col = (255, 255, 255) if correct else (190, 190, 190)
                    msg = f"-{dmg}" if correct else f"-{dmg} (Смени меч [R]!)"
                    floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, msg, col, 16))
                    witcher.attack_hitbox = pygame.Rect(0, 0, 0, 0)
                    break

            # Босс Леший
            if leshen.alive and witcher.attack_hitbox.colliderect(leshen.rect):
                mult = 1.0 if witcher.sword == "SILVER" else 0.2
                dmg = int(random.randint(35, 48) * mult)
                leshen.hp -= dmg
                camera.shake_power = 8.0
                blood_decals.append(BloodDecal(leshen.rect.centerx, leshen.rect.centery))
                floating_texts.append(FloatingText(leshen.rect.centerx, leshen.rect.top, f"-{dmg}", (255, 220, 160), 20))
                witcher.attack_hitbox = pygame.Rect(0, 0, 0, 0)
                if leshen.hp <= 0:
                    leshen.alive = False
                    quest_log["leshen"]["stage"] = 2
                    floating_texts.append(FloatingText(leshen.rect.centerx, leshen.rect.top, "ТРОФЕЙ ЛЕШЕГО ДОБЫТ!", COLOR_GOLD, 24))

        # Урон огнем от Игни
        for p in particles:
            if p.decay > 0.3:
                for e in enemies:
                    if distance_pts((p.x, p.y), e.rect.center) < 32:
                        e.hp -= 1.8
                        if random.random() < 0.08:
                            floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, "ОЖОГ", (255, 120, 20), 12))
                if leshen.alive and distance_pts((p.x, p.y), leshen.rect.center) < 55:
                    leshen.hp -= 3.0

        # Обновление магии, ловушек и врагов
        for yr in yrdens[:]:
            yr.update(enemies, leshen)
            if yr.life <= 0: yrdens.remove(yr)

        for rt in roots[:]:
            rt.update(witcher, floating_texts, blood_decals)
            if rt.active_time <= 0: roots.remove(rt)

        for e in enemies[:]:
            e.update(witcher, floating_texts, blood_decals)
            if e.hp <= 0:
                floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, "+ЛУТ", (200, 230, 110)))
                witcher.coins += random.randint(10, 25)
                enemies.remove(e)

        if leshen.alive:
            leshen.update(witcher, roots, floating_texts, particles)

    # Частицы и тексты
    for p in particles[:]:
        p.update()
        if p.radius <= 0: particles.remove(p)

    for ft in floating_texts[:]:
        ft.update()
        if ft.life <= 0: floating_texts.remove(ft)

    # =====================================================================
    # ОТРИСОВКА МИРА
    # =====================================================================
    screen.fill(COLOR_BG_SENSE if witcher.sense else COLOR_BG_NORMAL)

    # Следы ног и чудовища
    for fp in footprints:
        fp.draw(screen, witcher.sense)
    for mt in monster_tracks:
        mt.draw(screen, witcher.sense)

    # Пятна крови
    for b in blood_decals:
        b.draw(screen)

    # Дорога от деревни к логову
    p_start = camera.apply_pos(tr_start)
    p_end = camera.apply_pos(tr_end)
    pygame.draw.line(screen, (28, 25, 20) if witcher.sense else (42, 38, 30), p_start, p_end, 38)

    # Ловушки Ирден
    for yr in yrdens:
        yr.draw(screen)

    # Дома деревни
    for rect, surf in houses:
        screen.blit(surf, camera.apply_rect(rect))

    # Деревья и руины
    for obs in obstacles[len(houses):]:
        r = camera.apply_rect(obs)
        if obs.w > 70:
            pygame.draw.rect(screen, (75, 80, 80), r, border_radius=6)
            pygame.draw.rect(screen, (55, 60, 60), (r.x + 8, r.y + 8, r.w - 16, r.h - 16))
        else:
            pygame.draw.circle(screen, (16, 24, 16) if witcher.sense else (22, 36, 22), (r.centerx, r.centery), 40)
            pygame.draw.rect(screen, (48, 34, 24), (r.centerx - 7, r.centery - 7, 14, 20))

    # Корни босса
    for rt in roots:
        rt.draw(screen)

    # Лут и костры
    for obj in world_objects:
        obj.draw(screen, witcher.sense)

    # Персонажи и враги
    elder.draw(screen)
    herbalist.draw(screen)
    baron.draw(screen)

    for e in enemies:
        e.draw(screen, witcher.sense)

    leshen.draw(screen, witcher.sense)
    witcher.draw(screen)

    for p in particles:
        p.draw(screen)

    for ft in floating_texts:
        ft.draw(screen)

    # Оверлей Ведьмачьего чутья
    if witcher.sense:
        vignette = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
        vignette.fill((12, 6, 0, 85))
        pygame.draw.circle(vignette, (0, 0, 0, 0), (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2), 360)
        screen.blit(vignette, (0, 0))
        lbl = FONT_UI.render("[ ВЕДЬМАЧЬЕ ЧУТЬЕ АКТИВНО: ПОДСВЕЧЕНЫ СЛЕДЫ И СУНДУКИ ]", True, (255, 150, 0))
        screen.blit(lbl, (SCREEN_WIDTH // 2 - lbl.get_width() // 2, 70))

    # =====================================================================
    # КРУГЛАЯ МИНИ-КАРТА (РАДАР)
    # =====================================================================
    mm_center = (SCREEN_WIDTH - 105, 105)
    mm_rad = 75
    pygame.draw.circle(screen, (22, 26, 22), mm_center, mm_rad)
    pygame.draw.circle(screen, COLOR_UI_BORDER, mm_center, mm_rad, 3)

    r_scale = 0.065
    pygame.draw.circle(screen, (245, 245, 255), mm_center, 4) # Ведьмак

    for n in [elder, herbalist, baron]:
        rel = (pygame.Vector2(n.rect.center) - pygame.Vector2(witcher.rect.center)) * r_scale
        if rel.length() < mm_rad - 6:
            pygame.draw.circle(screen, COLOR_GOLD, (int(mm_center[0] + rel.x), int(mm_center[1] + rel.y)), 4)

    for e in enemies:
        rel = (pygame.Vector2(e.rect.center) - pygame.Vector2(witcher.rect.center)) * r_scale
        if rel.length() < mm_rad - 6:
            pygame.draw.circle(screen, (225, 45, 45), (int(mm_center[0] + rel.x), int(mm_center[1] + rel.y)), 3)

    if leshen.alive:
        rel = (pygame.Vector2(leshen.rect.center) - pygame.Vector2(witcher.rect.center)) * r_scale
        if rel.length() < mm_rad - 6:
            pygame.draw.circle(screen, (255, 0, 0), (int(mm_center[0] + rel.x), int(mm_center[1] + rel.y)), 6)

    # =====================================================================
    # HUD ИНТЕРФЕЙС В СТИЛЕ WITCHER 3
    # =====================================================================
    # Медальон Волка
    pygame.draw.circle(screen, (38, 38, 38), (60, 60), 34)
    pygame.draw.polygon(screen, (195, 195, 205), [(60, 38), (44, 66), (60, 60), (76, 66)])

    # Полоска здоровья (HP)
    pygame.draw.rect(screen, (32, 20, 20), (105, 38, 240, 16), border_radius=2)
    pygame.draw.rect(screen, COLOR_HP, (105, 38, int(240 * (witcher.hp / witcher.max_hp)), 16), border_radius=2)

    # Полоска энергии (Выносливость)
    pygame.draw.rect(screen, (32, 32, 20), (105, 58, 190, 10), border_radius=2)
    pygame.draw.rect(screen, COLOR_STAMINA, (105, 58, int(190 * (witcher.stamina / witcher.max_stamina)), 10), border_radius=2)

    # Полоска токсичности
    pygame.draw.rect(screen, (20, 32, 20), (105, 72, 130, 6), border_radius=2)
    pygame.draw.rect(screen, COLOR_TOXICITY, (105, 72, int(130 * min(1.0, witcher.toxicity / 100)), 6), border_radius=2)

    # Статусы и припасы
    sw_col = COLOR_SILVER if witcher.sword == "SILVER" else COLOR_STEEL
    screen.blit(FONT_HUD.render(f"Кроны: {witcher.coins} | Ласточка [F]: {witcher.potions} | Бомбы [G]: {witcher.bombs} | Травы: {witcher.herbs}", True, (240, 220, 160)), (105, 84))
    screen.blit(FONT_HUD.render(f"Меч [R]: {witcher.sword} | Инвентарь [I]", True, sw_col), (105, 100))

    hint_panel = FONT_TINY.render("[ЛКМ] Удар курсором | [ПКМ] Чутье | [1]Игни [2]Квен [3]Аард [4]Ирден [5]Аксий | [G]Бомба | [E]Действие", True, (190, 190, 190))
    screen.blit(hint_panel, (SCREEN_WIDTH // 2 - hint_panel.get_width() // 2, SCREEN_HEIGHT - 24))

    # Полоса босса
    if leshen.alive and distance_pts(witcher.rect.center, leshen.rect.center) < 800:
        bx = SCREEN_WIDTH // 2 - 240
        pygame.draw.rect(screen, (20, 20, 20), (bx, 24, 480, 16), border_radius=3)
        pygame.draw.rect(screen, (185, 25, 25), (bx, 24, int(480 * (leshen.hp / leshen.max_hp)), 16), border_radius=3)
        b_name = FONT_UI.render("ДРЕВНИЙ ЛЕШИЙ ИЗ ВЕЛЕНА", True, (245, 235, 215))
        screen.blit(b_name, (SCREEN_WIDTH // 2 - b_name.get_width() // 2, 44))

    # =====================================================================
    # ОКНО ДИАЛОГОВ
    # =====================================================================
    if dialog_active:
        box = pygame.Rect(120, SCREEN_HEIGHT - 190, SCREEN_WIDTH - 240, 160)
        pygame.draw.rect(screen, COLOR_UI_BG, box, border_radius=8)
        pygame.draw.rect(screen, COLOR_UI_BORDER, box, width=3, border_radius=8)
        screen.blit(FONT_DIALOG_BOLD.render(dialog_active["name"], True, COLOR_GOLD), (145, SCREEN_HEIGHT - 180))
        screen.blit(FONT_DIALOG.render(dialog_active["text"], True, (230, 230, 230)), (145, SCREEN_HEIGHT - 150))
        screen.blit(FONT_DIALOG.render(dialog_active["opt1"], True, (120, 225, 255)), (145, SCREEN_HEIGHT - 105))
        screen.blit(FONT_DIALOG.render(dialog_active["opt2"], True, (190, 190, 190)), (145, SCREEN_HEIGHT - 75))

    # =====================================================================
    # МЕНЮ (ИНВЕНТАРЬ, БЕСТИАРИЙ, КВЕСТЫ)
    # =====================================================================
    if menu_active:
        panel = pygame.Rect(140, 60, SCREEN_WIDTH - 280, SCREEN_HEIGHT - 120)
        pygame.draw.rect(screen, COLOR_UI_BG, panel, border_radius=10)
        pygame.draw.rect(screen, COLOR_UI_BORDER, panel, width=3, border_radius=10)

        screen.blit(FONT_HEADER.render("ВЕДЬМАЧИЙ ГРИМУАР", True, COLOR_GOLD), (panel.centerx - 120, panel.top + 18))
        tabs = FONT_UI.render("[1] ИНВЕНТАРЬ       [2] БЕСТИАРИЙ       [3] ЖУРНАЛ КВЕСТОВ", True, (210, 210, 210))
        screen.blit(tabs, (panel.centerx - tabs.get_width() // 2, panel.top + 55))
        pygame.draw.line(screen, (85, 75, 55), (panel.left + 25, panel.top + 80), (panel.right - 25, panel.top + 80), 2)

        if menu_tab == "INV":
            y = panel.top + 100
            screen.blit(FONT_UI.render("СНАРЯЖЕНИЕ В СУМКЕ:", True, COLOR_GOLD), (panel.left + 45, y))
            y += 30
            for item in witcher.inventory:
                screen.blit(FONT_DIALOG.render(f"• {item['name']}", True, (235, 235, 235)), (panel.left + 45, y))
                screen.blit(FONT_TINY.render(item['desc'], True, (165, 165, 165)), (panel.left + 45, y + 20))
                y += 45

            screen.blit(FONT_UI.render(f"Кроны: {witcher.coins} золотых", True, COLOR_GOLD), (panel.right - 260, panel.top + 100))
            screen.blit(FONT_DIALOG.render(f"Эликсиры: {witcher.potions} шт.", True, (100, 255, 120)), (panel.right - 260, panel.top + 130))
            screen.blit(FONT_DIALOG.render(f"Бомбы Самум: {witcher.bombs} шт.", True, (255, 220, 100)), (panel.right - 260, panel.top + 160))
            screen.blit(FONT_DIALOG.render(f"Ласточкина трава: {witcher.herbs} шт.", True, (100, 255, 120)), (panel.right - 260, panel.top + 190))

        elif menu_tab == "BESTIARY":
            y = panel.top + 100
            for mon, data in bestiary_database.items():
                screen.blit(FONT_UI.render(f"{mon} ({data['class']})", True, (245, 110, 90)), (panel.left + 45, y))
                screen.blit(FONT_DIALOG.render(data['desc'], True, (225, 225, 225)), (panel.left + 45, y + 22))
                screen.blit(FONT_TINY.render(f"Уязвимости: {data['weakness']}", True, (130, 225, 255)), (panel.left + 45, y + 44))
                y += 75

        elif menu_tab == "QUESTS":
            y = panel.top + 100
            for qid, q in quest_log.items():
                st_name = "ВЫПОЛНЕН" if q["stage"] == 3 else ("АКТИВЕН" if q["stage"] > 0 else "НЕ НАЧАТ")
                col = (100, 255, 100) if q["stage"] == 3 else (COLOR_GOLD if q["stage"] > 0 else (160, 160, 160))
                screen.blit(FONT_UI.render(f"• {q['name']} — [{st_name}]", True, col), (panel.left + 45, y))
                screen.blit(FONT_DIALOG.render(q["desc"], True, (210, 210, 210)), (panel.left + 45, y + 22))
                y += 65

        screen.blit(FONT_TINY.render("[ESC / I] Закрыть меню", True, (160, 160, 160)), (panel.centerx - 55, panel.bottom - 26))

    pygame.display.flip()
    clock.tick(FPS)

pygame.quit()
sys.exit()