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


import pygame
import random
import math
import sys

pygame.init()

# Размеры экрана и мира
SCREEN_W, SCREEN_H = 1100, 720
WORLD_W, WORLD_H = 4000, 3200
FPS = 60

screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
pygame.display.set_caption("The Witcher 2D: Chronicles of Velen")
clock = pygame.time.Clock()

# Шрифты
f_tiny = pygame.font.SysFont("georgia", 11)
f_hud = pygame.font.SysFont("consolas", 12, bold=True)
f_ui = pygame.font.SysFont("georgia", 14, bold=True)
f_dialog = pygame.font.SysFont("georgia", 16)
f_dialog_name = pygame.font.SysFont("georgia", 18, bold=True)
f_title = pygame.font.SysFont("georgia", 22, bold=True)
f_dmg = pygame.font.SysFont("georgia", 16, bold=True)

# ----------------- КАМЕРА -----------------
class Camera:
    def __init__(self):
        self.x = 0
        self.y = 0
        self.shake = 0

    def update(self, target_pos):
        tx = target_pos[0] - SCREEN_W // 2
        ty = target_pos[1] - SCREEN_H // 2
        self.x += (tx - self.x) * 0.12
        self.y += (ty - self.y) * 0.12
        self.x = max(0, min(self.x, WORLD_W - SCREEN_W))
        self.y = max(0, min(self.y, WORLD_H - SCREEN_H))

        if self.shake > 0:
            self.x += random.randint(-self.shake, self.shake)
            self.y += random.randint(-self.shake, self.shake)
            self.shake -= 1

    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))

camera = Camera()

# ----------------- ПАРТИКЛЫ И ВИЗУАЛ -----------------
class Particle:
    def __init__(self, x, y, vx, vy, color, radius, decay, p_type="circle"):
        self.x, self.y = x, y
        self.vx, self.vy = vx, 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:
            p = camera.apply_pos((self.x, self.y))
            if self.p_type == "circle":
                pygame.draw.circle(surf, self.color, p, 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, (p[0] - int(self.radius * 2), p[1] - int(self.radius * 2)))

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

    def update(self):
        self.y -= 0.8
        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, self.y = x, y
        self.size = random.randint(8, 16)
        self.color = (random.randint(90, 130), 10, 15)

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

# ----------------- ПРОЦЕДУРНАЯ ГРАФИКА -----------------
def generate_witcher_surface(step, facing, attacking, sword_type):
    surf = pygame.Surface((48, 54), pygame.SRCALPHA)
    leg = int(math.sin(step * 0.25) * 5)
    # Ноги и сапоги
    pygame.draw.rect(surf, (25, 20, 15), (14, 38 + leg, 7, 14))
    pygame.draw.rect(surf, (25, 20, 15), (27, 38 - leg, 7, 14))
    # Броня Школы Волка
    pygame.draw.rect(surf, (40, 35, 30), (12, 16, 24, 24), border_radius=4)
    pygame.draw.rect(surf, (75, 45, 30), (16, 18, 16, 14))
    pygame.draw.circle(surf, (180, 180, 180), (24, 22), 3) # Медальон Волка
    # Мечи на спине
    pygame.draw.line(surf, (140, 140, 145), (8, 10), (32, 44), 3) # Сталь
    pygame.draw.line(surf, (210, 230, 250), (40, 10), (16, 44), 3) # Серебро
    # Голова и седые волосы
    pygame.draw.circle(surf, (215, 185, 155), (24, 12), 9)
    pygame.draw.arc(surf, (245, 245, 250), (14, 2, 20, 18), 0, 3.14, 5)
    pygame.draw.circle(surf, (245, 245, 250), (24, 5), 7)
    # Кошачьи глаза
    dx, dy = int(facing.x * 3), int(facing.y * 3)
    pygame.draw.circle(surf, (255, 195, 0), (22 + dx, 12 + dy), 2)
    pygame.draw.circle(surf, (255, 195, 0), (27 + dx, 12 + dy), 2)

    # Удар клинком
    if attacking:
        col = (210, 240, 255) if sword_type == "SILVER" else (220, 200, 160)
        pygame.draw.line(surf, col, (24, 24), (24 + int(facing.x * 28), 24 + int(facing.y * 28)), 4)
    return surf

def generate_npc_surface(role="elder"):
    surf = pygame.Surface((40, 50), pygame.SRCALPHA)
    pygame.draw.rect(surf, (50, 40, 30), (12, 36, 7, 14))
    pygame.draw.rect(surf, (50, 40, 30), (21, 36, 7, 14))
    if role == "elder":
        pygame.draw.rect(surf, (180, 170, 150), (10, 16, 20, 22), border_radius=2)
        pygame.draw.circle(surf, (215, 180, 150), (20, 12), 8)
        pygame.draw.ellipse(surf, (170, 160, 150), (14, 12, 12, 10)) # Седая борода
    elif role == "herbalist":
        pygame.draw.rect(surf, (50, 110, 70), (10, 16, 20, 22), border_radius=3)
        pygame.draw.circle(surf, (225, 190, 160), (20, 12), 8)
        pygame.draw.arc(surf, (120, 40, 30), (12, 4, 16, 14), 0, 3.14, 4) # Рыжие волосы
    return surf

# ----------------- ИГРОК -----------------
class Witcher:
    def __init__(self, x, y):
        self.rect = pygame.Rect(x, y, 34, 44)
        self.max_hp = 200
        self.hp = 200
        self.max_stamina = 100
        self.stamina = 100
        self.toxicity = 0
        self.speed = 4.3
        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.coins = 120
        self.potions = 3
        self.herbs = 4
        self.trophies = []
        self.inventory = [
            {"name": "Серебряный меч Школы Волка", "type": "weapon", "desc": "Урон +40 по чудовищам"},
            {"name": "Стальной меч Новиграда", "type": "weapon", "desc": "Урон +35 по людям"},
            {"name": "Ласточка (Эликсир)", "type": "potion", "desc": "Быстро лечит раны (+65 HP)"},
            {"name": "Ласточкина трава", "type": "herb", "desc": "Ингредиент для алхимии"},
        ]

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

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

        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):
        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
            # Ось 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_W, WORLD_H))

    def attack_mouse(self, mouse_world_pos):
        if not self.attacking and self.stamina >= 12:
            self.attacking = True
            self.stamina -= 12
            self.atk_timer = 15
            self.combo = (self.combo % 3) + 1

            # Вектор удара в сторону мыши
            dir_vec = pygame.Vector2(mouse_world_pos) - pygame.Vector2(self.rect.center)
            if dir_vec.length() > 0:
                self.facing = dir_vec.normalize()

            reach = 42
            ax = self.rect.centerx + int(self.facing.x * reach) - 28
            ay = self.rect.centery + int(self.facing.y * reach) - 28
            self.attack_hitbox = pygame.Rect(ax, ay, 56, 56)

    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 (Ласточка)", (50, 255, 120)))
        elif self.toxicity > 80:
            texts.append(FloatingText(self.rect.centerx, self.rect.top, "Высокая токсичность!", (255, 50, 50)))

    def draw(self, surf):
        r = camera.apply_rect(self.rect)
        pygame.draw.ellipse(surf, (15, 20, 15, 120), (r.centerx - 16, r.bottom - 6, 32, 12))
        spr = generate_witcher_surface(self.step, self.facing, self.attacking, self.sword)
        surf.blit(spr, (r.centerx - 24, r.centery - 27))
        if self.quen:
            pygame.draw.circle(surf, (255, 215, 0), r.center, 34, 2)

# ----------------- ВРАГИ: УТОПЕЦ, БАНДИТ, ЛЕШИЙ -----------------
class Enemy:
    def __init__(self, x, y, e_type):
        self.rect = pygame.Rect(x, y, 32, 42)
        self.e_type = e_type  # "DROWNER", "BANDIT"
        self.is_monster = True if e_type == "DROWNER" else False
        self.hp = 75 if e_type == "DROWNER" else 90
        self.max_hp = self.hp
        self.speed = 2.7 if e_type == "DROWNER" else 2.4
        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 < 500:
            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, "КВЕН СПАС!", (255, 220, 50)))
            else:
                dmg = random.randint(16, 26)
                player.hp -= dmg
                camera.shake = 6
                texts.append(FloatingText(player.rect.centerx, player.rect.top, f"-{dmg}", (255, 40, 40)))
                decals.append(BloodDecal(player.rect.centerx, player.rect.centery))

    def draw(self, surf, sense):
        r = camera.apply_rect(self.rect)
        if self.e_type == "DROWNER":
            pygame.draw.rect(surf, (40, 115, 120), r, border_radius=4)
            pygame.draw.circle(surf, (255, 50, 50), (r.centerx - 4, r.y + 8), 2)
            pygame.draw.circle(surf, (255, 50, 50), (r.centerx + 4, r.y + 8), 2)
        else: # BANDIT
            pygame.draw.rect(surf, (90, 55, 40), r, border_radius=3)
            pygame.draw.circle(surf, (220, 180, 140), (r.centerx, r.y + 8), 6) # Голова
            pygame.draw.line(surf, (180, 180, 180), (r.right, r.centery), (r.right + 10, r.centery - 6), 3)

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

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

        # HP бар
        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, (220, 40, 40), (r.x, r.y - 8, int(self.rect.w * pct), 4))


class AncientLeshen:
    def __init__(self, x, y):
        self.rect = pygame.Rect(x, y, 76, 100)
        self.max_hp = 700
        self.hp = 700
        self.alive = True
        self.teleport_cd = 200
        self.root_cd = 130
        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 < 750:
            self.teleport_cd = 240
            for _ in range(40):
                particles.append(Particle(self.rect.centerx, self.rect.centery, random.uniform(-4, 4), random.uniform(-4, 4), (40, 40, 40), random.randint(6, 12), 0.2, "glow"))
            ang = random.uniform(0, 6.28)
            self.rect.x = int(player.rect.centerx + math.cos(ang) * 220)
            self.rect.y = int(player.rect.centery + math.sin(ang) * 220)
            texts.append(FloatingText(self.rect.centerx, self.rect.top, "*Исчез в тумане*", (180, 220, 180), 16))

        # Корни
        if self.root_cd <= 0 and dist < 650:
            self.root_cd = 160
            roots.append(RootTrap(player.rect.centerx, player.rect.centery))
            texts.append(FloatingText(self.rect.centerx, self.rect.top, "КОРНИ ЗЕМЛИ!", (130, 200, 80), 18))

        # Движение
        spd = 1.6 if self.slow_timer == 0 else 0.7
        if dist > 40:
            d = v.normalize()
            self.rect.x += int(d.x * spd)
            self.rect.y += int(d.y * spd)

    def draw(self, surf, sense):
        if not self.alive: return
        r = camera.apply_rect(self.rect)
        pygame.draw.ellipse(surf, (0, 0, 0, 130), (r.centerx - 30, r.bottom - 10, 60, 20))
        pygame.draw.rect(surf, (40, 26, 18), r, border_radius=6)
        # Олений череп и рога
        pygame.draw.polygon(surf, (220, 215, 190), [(r.centerx, r.top + 34), (r.left + 12, r.top + 6), (r.right - 12, r.top + 6)])
        pygame.draw.line(surf, (30, 20, 15), (r.left + 14, r.top + 6), (r.left - 18, r.top - 24), 4)
        pygame.draw.line(surf, (30, 20, 15), (r.right - 14, r.top + 6), (r.right + 18, r.top - 24), 4)
        pygame.draw.circle(surf, (180, 255, 210), (r.centerx - 7, r.top + 16), 3)
        pygame.draw.circle(surf, (180, 255, 210), (r.centerx + 7, r.top + 16), 3)

        if sense:
            glow = pygame.Surface((r.w + 24, r.h + 24), pygame.SRCALPHA)
            pygame.draw.rect(glow, (255, 30, 0, 170), glow.get_rect(), width=4, border_radius=8)
            surf.blit(glow, (r.x - 12, r.y - 12))

class RootTrap:
    def __init__(self, x, y):
        self.rect = pygame.Rect(x - 35, y - 35, 70, 70)
        self.timer = 40
        self.active_time = 35
        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, "КВЕН РАЗБИТ", (255, 220, 50)))
                else:
                    dmg = random.randint(32, 45)
                    player.hp -= dmg
                    camera.shake = 10
                    texts.append(FloatingText(player.rect.centerx, player.rect.top, f"-{dmg} КОРНИ", (220, 40, 40), 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, (140, 60, 40), r.center, int(30 * (1 - self.timer / 40)), 2)
        else:
            pygame.draw.circle(surf, (40, 60, 25), r.center, 32)
            for a in [0, 1.2, 2.4, 3.6, 4.8]:
                ex = r.centerx + int(math.cos(a) * 30)
                ey = r.centery + int(math.sin(a) * 30)
                pygame.draw.line(surf, (60, 35, 20), r.center, (ex, ey), 4)

class YrdenTrap:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.radius = 130
        self.life = 360

    def update(self, enemies, boss):
        self.life -= 1
        for e in enemies:
            if math.hypot(self.x - e.rect.centerx, self.y - e.rect.centery) < self.radius:
                e.slow_timer = 25
        if boss.alive and math.hypot(self.x - boss.rect.centerx, self.y - boss.rect.centery) < 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, (180, 50, 240, 50), (self.radius, self.radius), self.radius)
        pygame.draw.circle(s, (220, 120, 255, 140), (self.radius, self.radius), self.radius, 2)
        surf.blit(s, (pos[0] - self.radius, pos[1] - self.radius))

# ----------------- КВЕСТОВЫЕ ДАННЫЕ И МИР -----------------
class QuestManager:
    def __init__(self):
        self.quests = {
            "leshen": {"name": "Заказ: Дух Древнего Леса", "stage": 0, "desc": "Староста Залипья просит уничтожить чудовище в восточной чаще."},
            "bandits": {"name": "Очистка тракта", "stage": 0, "desc": "Травница Томира просит вернуть украденные травы из лагеря дезертиров на юге."}
        }

quest_log = QuestManager()

class NPC:
    def __init__(self, x, y, name, role):
        self.rect = pygame.Rect(x, y, 32, 44)
        self.name = name
        self.role = role
        self.sprite = generate_npc_surface(role)

    def draw(self, surf):
        r = camera.apply_rect(self.rect)
        surf.blit(self.sprite, (r.x - 4, r.y - 6))
        # Иконка квеста
        pygame.draw.circle(surf, (240, 200, 20), (r.centerx, r.top - 14), 7)
        surf.blit(f_hud.render("!", True, (10, 10, 10)), (r.centerx - 3, r.top - 21))

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

    def draw(self, surf, sense):
        if self.looted: return
        r = camera.apply_rect(self.rect)
        if self.obj_type == "HERB":
            pygame.draw.circle(surf, (50, 200, 80), r.center, 7)
            pygame.draw.circle(surf, (230, 255, 100), (r.centerx, r.centery - 2), 4)
        else: # CHEST
            pygame.draw.rect(surf, (110, 70, 30), r, border_radius=3)
            pygame.draw.rect(surf, (200, 170, 50), (r.x + 2, r.y + 10, r.w - 4, 4))

        if sense:
            glow = pygame.Surface((r.w + 10, r.h + 10), pygame.SRCALPHA)
            col = (255, 140, 0, 160) if self.obj_type == "CHEST" else (100, 255, 120, 160)
            pygame.draw.rect(glow, col, glow.get_rect(), width=2, border_radius=4)
            surf.blit(glow, (r.x - 5, r.y - 5))

# Инициализация объектов мира
witcher = Witcher(600, 600)
elder = NPC(680, 580, "Староста Богдан", "elder")
herbalist = NPC(520, 720, "Травница Томира", "herbalist")
leshen = AncientLeshen(3300, 1800)

houses = [
    pygame.Rect(450, 420, 150, 120),
    pygame.Rect(720, 400, 170, 130),
    pygame.Rect(580, 800, 160, 120),
]

obstacles = list(houses)
for _ in range(75): # Деревья
    obstacles.append(pygame.Rect(random.randint(1200, WORLD_W - 200), random.randint(100, WORLD_H - 200), 55, 55))
for _ in range(20): # Руины
    obstacles.append(pygame.Rect(random.randint(2400, WORLD_W - 400), random.randint(1200, WORLD_H - 400), 90, 70))

# Враги
enemies = []
for _ in range(14): # Утопцы на болотах
    enemies.append(Enemy(random.randint(1400, 2600), random.randint(400, 1600), "DROWNER"))
for _ in range(8): # Бандиты на юге
    enemies.append(Enemy(random.randint(800, 1800), random.randint(2200, 3000), "BANDIT"))

# Лут и травы
world_objects = []
for _ in range(25):
    world_objects.append(InteractObject(random.randint(300, WORLD_W - 300), random.randint(300, WORLD_H - 300), "HERB"))
for _ in range(10):
    world_objects.append(InteractObject(random.randint(500, WORLD_W - 400), random.randint(500, WORLD_H - 400), "CHEST"))

# Кровавые следы Лешего (для Ведьмачьего чутья)
clues = []
start_p = pygame.Vector2(850, 550)
end_p = pygame.Vector2(3250, 1780)
for i in range(25):
    p = start_p.lerp(end_p, i / 25.0)
    clues.append(pygame.Vector2(p.x + random.randint(-20, 20), p.y + random.randint(-20, 20)))

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

# Состояния игры
menu_open = False
current_tab = "INV" # "INV", "BESTIARY", "QUESTS"
dialog_active = None

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

# ----------------- ИГРОВОЙ ЦИКЛ -----------------
running = True
while running:
    mouse_buttons = pygame.mouse.get_pressed()
    mouse_x, mouse_y = pygame.mouse.get_pos()
    mouse_world = (mouse_x + camera.x, mouse_y + camera.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_open = not menu_open

            # Переключение вкладок меню
            if menu_open:
                if event.key == pygame.K_1: current_tab = "INV"
                if event.key == pygame.K_2: current_tab = "BESTIARY"
                if event.key == pygame.K_3: current_tab = "QUESTS"
                if event.key == pygame.K_ESCAPE: menu_open = False
                continue

            # Диалоговый выбор
            if dialog_active:
                if event.key == pygame.K_1:
                    cb = dialog_active["opt1_cb"]
                    if cb: cb()
                    dialog_active = None
                elif event.key == pygame.K_2 or event.key == pygame.K_ESCAPE:
                    dialog_active = None
                continue

            # Взаимодействие (E)
            if event.key == pygame.K_e:
                # Разговор со Старостой
                if math.hypot(witcher.rect.centerx - elder.rect.centerx, witcher.rect.centery - elder.rect.centery) < 70:
                    st = quest_log.quests["leshen"]["stage"]
                    if st == 0:
                        def accept():
                            quest_log.quests["leshen"]["stage"] = 1
                            floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "ЗАКАЗ ПРИНЯТ!", (255, 215, 0), 18))
                        dialog_active = {
                            "name": elder.name,
                            "text": "Ведьмак! В восточной чаще завёлся Леший, дровосеки пропадают. Убей ирода!",
                            "opt1": "1. Беру заказ на чудовище. [250 крон]", "opt1_cb": accept,
                            "opt2": "2. В другой раз."
                        }
                    elif st == 2:
                        def reward():
                            quest_log.quests["leshen"]["stage"] = 3
                            witcher.coins += 250
                            floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+250 КРОН", (255, 215, 0), 22))
                        dialog_active = {
                            "name": elder.name,
                            "text": "Хвала богам, ты снял проклятье леса! Вот твои кроны до единой, Белый Волк!",
                            "opt1": "1. Забрать награду (250 крон)", "opt1_cb": reward,
                            "opt2": "2. Позже."
                        }
                    else:
                        dialog_active = {"name": elder.name, "text": "Иди по следам когтей на восток с чутьем!", "opt1": "1. Ухожу.", "opt1_cb": None, "opt2": "2. Закрыть"}

                # Разговор с Травницей
                elif math.hypot(witcher.rect.centerx - herbalist.rect.centerx, witcher.rect.centery - herbalist.rect.centery) < 70:
                    def buy_swallow():
                        if witcher.coins >= 35:
                            witcher.coins -= 35
                            witcher.potions += 1
                            floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+1 Ласточка", (100, 255, 140)))
                    dialog_active = {
                        "name": herbalist.name,
                        "text": "Здравствуй, Геральт. Могу сварить тебе 'Ласточку' за 35 крон.",
                        "opt1": "1. Купить 'Ласточку' (-35 крон)", "opt1_cb": buy_swallow,
                        "opt2": "2. До встречи."
                    }

                # Сбор трав и сундуков
                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 Трава", (100, 255, 100)))
                        else:
                            c = random.randint(40, 90)
                            witcher.coins += c
                            floating_texts.append(FloatingText(obj.rect.centerx, obj.rect.top, f"+{c} Крон из сундука!", (255, 215, 0), 18))

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

            # Пить Ласточку (F)
            if event.key == pygame.K_f:
                witcher.drink_potion(floating_texts)

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

            # --- 5 ЗНАКОВ ВЕДЬМАКА ---
            if not menu_open and not dialog_active:
                # 1. ИГНИ
                if event.key == pygame.K_1 and witcher.stamina >= 35:
                    witcher.stamina -= 35
                    camera.shake = 5
                    for _ in range(45):
                        ang = (random.random() - 0.5) * 0.95
                        v = witcher.facing.rotate_rad(ang) * random.uniform(6, 13)
                        particles.append(Particle(witcher.rect.centerx, witcher.rect.centery, v.x, v.y, (255, random.randint(70, 150), 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, "КВЕН", (255, 215, 0), 16))

                # 3. ААРД
                if event.key == pygame.K_3 and witcher.stamina >= 35:
                    witcher.stamina -= 35
                    camera.shake = 7
                    for _ in range(35):
                        ang = (random.random() - 0.5) * 0.8
                        v = witcher.facing.rotate_rad(ang) * random.uniform(8, 16)
                        particles.append(Particle(witcher.rect.centerx, witcher.rect.centery, v.x, v.y, (180, 220, 255), random.randint(4, 9), 0.3, "glow"))
                    for e in enemies:
                        if math.hypot(e.rect.centerx - witcher.rect.centerx, e.rect.centery - witcher.rect.centery) < 200:
                            e.rect.x += int(witcher.facing.x * 80)
                            e.rect.y += int(witcher.facing.y * 80)
                            e.hp -= 20
                            e.stun_timer = 30
                            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, "ИРДЕН", (200, 100, 255)))

                # 5. АКСИЙ
                if event.key == pygame.K_5 and witcher.stamina >= 35:
                    witcher.stamina -= 35
                    target = None
                    for e in enemies:
                        if math.hypot(e.rect.centerx - witcher.rect.centerx, e.rect.centery - witcher.rect.centery) < 250:
                            target = e
                            break
                    if target:
                        target.stun_timer = 180 # 3 секунды гипноза
                        floating_texts.append(FloatingText(target.rect.centerx, target.rect.top, "АКСИЙ: ОДУРМАНЕН", (100, 255, 200), 16))

        # АТАКА МЫШЬЮ (ЛКМ)
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and not menu_open and not dialog_active:
            witcher.attack_mouse(mouse_world)

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

    # Обновление движения и логики
    if not menu_open 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)
        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(28, 40) * mult)
                    e.hp -= dmg
                    camera.shake = 5
                    blood_decals.append(BloodDecal(e.rect.centerx, e.rect.centery))
                    msg = f"-{dmg}" if correct else f"-{dmg} (Смени меч [R]!)"
                    col = (255, 255, 255) if correct else (190, 190, 190)
                    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(32, 45) * mult)
                leshen.hp -= dmg
                camera.shake = 8
                blood_decals.append(BloodDecal(leshen.rect.centerx, leshen.rect.centery))
                floating_texts.append(FloatingText(leshen.rect.centerx, leshen.rect.top, f"-{dmg}", (255, 220, 150), 20))
                witcher.attack_hitbox = pygame.Rect(0, 0, 0, 0)
                if leshen.hp <= 0:
                    leshen.alive = False
                    quest_log.quests["leshen"]["stage"] = 2
                    floating_texts.append(FloatingText(leshen.rect.centerx, leshen.rect.top, "ТРОФЕЙ ЛЕШЕГО ДОБЫТ!", (255, 215, 0), 24))

        # Огонь Игни
        for p in particles:
            if p.decay > 0.3:
                for e in enemies:
                    if math.hypot(p.x - e.rect.centerx, p.y - e.rect.centery) < 30:
                        e.hp -= 1.6
                        if random.random() < 0.08:
                            floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, "ОЖОГ", (255, 120, 20), 12))
                if leshen.alive and math.hypot(p.x - leshen.rect.centerx, p.y - leshen.rect.centery) < 50:
                    leshen.hp -= 2.8 # Леший очень уязвим к огню

        # Обновление магии и врагов
        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, 220, 100)))
                witcher.coins += random.randint(8, 20)
                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)

    # ----------------- ОТРИСОВКА МИРА -----------------
    # Цвет земли зависит от Ведьмачьего чутья
    bg_color = (25, 27, 25) if witcher.sense else (45, 55, 42)
    screen.fill(bg_color)

    # Кровь на земле
    for b in blood_decals:
        b.draw(screen)

    # Дороги и тропы
    t_start = camera.apply_pos(start_p)
    t_end = camera.apply_pos(end_p)
    pygame.draw.line(screen, (35, 32, 26) if not witcher.sense else (18, 18, 18), t_start, t_end, 36)

    # Круги Ирден
    for yr in yrdens:
        yr.draw(screen)

    # Дома
    for h in houses:
        hr = camera.apply_rect(h)
        pygame.draw.rect(screen, (75, 50, 30), hr, border_radius=4)
        pygame.draw.polygon(screen, (140, 115, 60), [(hr.left - 10, hr.top), (hr.centerx, hr.top - 35), (hr.right + 10, hr.top)])
        pygame.draw.rect(screen, (40, 25, 15), (hr.centerx - 14, hr.bottom - 36, 28, 36))

    # Деревья и руины
    for obs in obstacles[len(houses):]:
        r = camera.apply_rect(obs)
        if obs.w > 65: # Руины
            pygame.draw.rect(screen, (70, 75, 75), r, border_radius=6)
            pygame.draw.rect(screen, (50, 55, 55), (r.x + 8, r.y + 8, r.w - 16, r.h - 16))
        else: # Дерево
            pygame.draw.circle(screen, (22, 34, 22) if not witcher.sense else (15, 15, 15), (r.centerx, r.centery), 38)
            pygame.draw.rect(screen, (50, 35, 25), (r.centerx - 7, r.centery - 7, 14, 18))

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

    # Интерактивные объекты (травы, сундуки)
    for obj in world_objects:
        obj.draw(screen, witcher.sense)

    # Следы для ведьмачьего чутья
    if witcher.sense:
        for cl in clues:
            cp = camera.apply_pos((cl.x, cl.y))
            pygame.draw.circle(screen, (255, 40, 40), cp, 5)
            pygame.draw.circle(screen, (255, 160, 40), cp, 12, 1)

    # NPC
    elder.draw(screen)
    herbalist.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:
        vign = pygame.Surface((SCREEN_W, SCREEN_H), pygame.SRCALPHA)
        vign.fill((10, 5, 0, 80))
        pygame.draw.circle(vign, (0, 0, 0, 0), (SCREEN_W // 2, SCREEN_H // 2), 340)
        screen.blit(vign, (0, 0))
        s_lbl = f_ui.render("[ ВЕДЬМАЧЬЕ ЧУТЬЕ АКТИВНО: ПОДСВЕЧЕНЫ СЛЕДЫ И ВРАГИ ]", True, (255, 150, 0))
        screen.blit(s_lbl, (SCREEN_W // 2 - s_lbl.get_width() // 2, 70))

    # ----------------- КРУГЛЫЙ РАДАР (МИНИ-КАРТА) -----------------
    mm_c = (SCREEN_W - 95, 95)
    mm_rad = 70
    pygame.draw.circle(screen, (20, 24, 20), mm_c, mm_rad)
    pygame.draw.circle(screen, (160, 140, 95), mm_c, mm_rad, 3)

    r_scale = 0.07
    pygame.draw.circle(screen, (250, 250, 255), mm_c, 4) # Ведьмак

    # Метки NPC на мини-карте
    for n in [elder, herbalist]:
        rel = (pygame.Vector2(n.rect.center) - pygame.Vector2(witcher.rect.center)) * r_scale
        if rel.length() < mm_rad - 6:
            pygame.draw.circle(screen, (255, 215, 0), (int(mm_c[0] + rel.x), int(mm_c[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, (220, 40, 40), (int(mm_c[0] + rel.x), int(mm_c[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_c[0] + rel.x), int(mm_c[1] + rel.y)), 6)

    # ----------------- HUD ИНТЕРФЕЙС -----------------
    # Медальон Волка
    pygame.draw.circle(screen, (35, 35, 35), (55, 55), 32)
    pygame.draw.polygon(screen, (190, 190, 205), [(55, 35), (40, 60), (55, 55), (70, 60)])

    # Полоски здоровья, выносливости, токсичности
    pygame.draw.rect(screen, (30, 20, 20), (95, 35, 220, 16), border_radius=2)
    pygame.draw.rect(screen, (210, 35, 35), (95, 35, int(220 * (witcher.hp / witcher.max_hp)), 16), border_radius=2)

    pygame.draw.rect(screen, (30, 30, 20), (95, 54, 180, 10), border_radius=2)
    pygame.draw.rect(screen, (240, 190, 25), (95, 54, int(180 * (witcher.stamina / witcher.max_stamina)), 10), border_radius=2)

    pygame.draw.rect(screen, (20, 30, 20), (95, 68, 120, 6), border_radius=2)
    pygame.draw.rect(screen, (40, 210, 80), (95, 68, int(120 * min(1.0, witcher.toxicity / 100)), 6), border_radius=2)

    # Статусная строка
    sw_col = (200, 230, 255) if witcher.sword == "SILVER" else (240, 210, 160)
    screen.blit(f_hud.render(f"Кроны: {witcher.coins} | Ласточка [F]: {witcher.potions} | Травы: {witcher.herbs}", True, (240, 220, 160)), (95, 80))
    screen.blit(f_hud.render(f"Меч [R]: {witcher.sword} | Инвентарь [I]", True, sw_col), (95, 96))

    # Панель Знаков
    ctrl_hint = f_tiny.render("[ЛКМ] Удар курсором | [ПКМ] Чутье | [1]Игни [2]Квен [3]Аард [4]Ирден [5]Аксий | [E]Действие", True, (190, 190, 190))
    screen.blit(ctrl_hint, (SCREEN_W // 2 - ctrl_hint.get_width() // 2, SCREEN_H - 24))

    # Полоса HP Босса
    if leshen.alive and math.hypot(witcher.rect.centerx - leshen.rect.centerx, witcher.rect.centery - leshen.rect.centery) < 750:
        bx = SCREEN_W // 2 - 220
        pygame.draw.rect(screen, (20, 20, 20), (bx, 20, 440, 16), border_radius=3)
        pygame.draw.rect(screen, (180, 25, 25), (bx, 20, int(440 * (leshen.hp / leshen.max_hp)), 16), border_radius=3)
        b_name = f_ui.render("ДРЕВНИЙ ЛЕШИЙ ИЗ ЗАЛИПЬЯ", True, (240, 230, 210))
        screen.blit(b_name, (SCREEN_W // 2 - b_name.get_width() // 2, 40))

    # ----------------- ОКНО ДИАЛОГОВ -----------------
    if dialog_active:
        box = pygame.Rect(100, SCREEN_H - 180, SCREEN_W - 200, 150)
        pygame.draw.rect(screen, (20, 18, 16), box, border_radius=8)
        pygame.draw.rect(screen, (150, 130, 85), box, width=3, border_radius=8)
        screen.blit(f_dialog_name.render(dialog_active["name"], True, (240, 205, 80)), (120, SCREEN_H - 170))
        screen.blit(f_dialog.render(dialog_active["text"], True, (230, 230, 230)), (120, SCREEN_H - 140))
        screen.blit(f_dialog.render(dialog_active["opt1"], True, (120, 220, 255)), (120, SCREEN_H - 95))
        screen.blit(f_dialog.render(dialog_active["opt2"], True, (180, 180, 180)), (120, SCREEN_H - 68))

    # ----------------- БОЛЬШОЕ МЕНЮ (I): ИНВЕНТАРЬ, БЕСТИАРИЙ, КВЕСТЫ -----------------
    if menu_open:
        p_rect = pygame.Rect(120, 70, SCREEN_W - 240, SCREEN_H - 140)
        pygame.draw.rect(screen, (18, 16, 15), p_rect, border_radius=10)
        pygame.draw.rect(screen, (160, 135, 80), p_rect, width=3, border_radius=10)

        # Шапка вкладок
        screen.blit(f_title.render("ВЕДЬМАЧИЙ ГРИМУАР", True, (240, 210, 90)), (p_rect.centerx - 120, p_rect.top + 18))
        tabs_txt = f_ui.render("[1] ИНВЕНТАРЬ       [2] БЕСТИАРИЙ       [3] ЖУРНАЛ КВЕСТОВ", True, (200, 200, 200))
        screen.blit(tabs_txt, (p_rect.centerx - tabs_txt.get_width() // 2, p_rect.top + 55))
        pygame.draw.line(screen, (80, 70, 50), (p_rect.left + 20, p_rect.top + 80), (p_rect.right - 20, p_rect.top + 80), 2)

        # Контент вкладок
        if current_tab == "INV":
            y = p_rect.top + 100
            screen.blit(f_ui.render("ПРЕДМЕТЫ В СУМКЕ:", True, (255, 215, 0)), (p_rect.left + 40, y))
            y += 30
            for item in witcher.inventory:
                screen.blit(f_dialog.render(f"• {item['name']}", True, (230, 230, 230)), (p_rect.left + 40, y))
                screen.blit(f_tiny.render(item['desc'], True, (160, 160, 160)), (p_rect.left + 40, y + 20))
                y += 45

            # Статы снаряжения
            screen.blit(f_ui.render(f"Кроны: {witcher.coins} золотых", True, (255, 215, 0)), (p_rect.right - 240, p_rect.top + 100))
            screen.blit(f_dialog.render(f"Эликсиры: {witcher.potions} шт.", True, (100, 255, 120)), (p_rect.right - 240, p_rect.top + 130))
            screen.blit(f_dialog.render(f"Собрано трав: {witcher.herbs} шт.", True, (100, 255, 120)), (p_rect.right - 240, p_rect.top + 160))

        elif current_tab == "BESTIARY":
            y = p_rect.top + 100
            for monster, data in bestiary_data.items():
                screen.blit(f_ui.render(f"{monster} ({data['class']})", True, (240, 100, 80)), (p_rect.left + 40, y))
                screen.blit(f_dialog.render(data['desc'], True, (220, 220, 220)), (p_rect.left + 40, y + 22))
                screen.blit(f_tiny.render(f"Уязвимости: {data['weakness']}", True, (120, 220, 255)), (p_rect.left + 40, y + 44))
                y += 75

        elif current_tab == "QUESTS":
            y = p_rect.top + 100
            for q_id, q in quest_log.quests.items():
                st_txt = "ВЫПОЛНЕН" if q["stage"] == 3 else ("АКТИВЕН" if q["stage"] > 0 else "НЕ НАЧАТ")
                col = (100, 255, 100) if q["stage"] == 3 else ((255, 215, 0) if q["stage"] > 0 else (160, 160, 160))
                screen.blit(f_ui.render(f"• {q['name']} — [{st_txt}]", True, col), (p_rect.left + 40, y))
                screen.blit(f_dialog.render(q["desc"], True, (200, 200, 200)), (p_rect.left + 40, y + 22))
                y += 65

        screen.blit(f_tiny.render("[ESC / I] Закрыть меню", True, (160, 160, 160)), (p_rect.centerx - 50, p_rect.bottom - 25))

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

pygame.quit()
sys.exit()