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


from ursina import *
import math
import random

app = Ursina(title="The Witcher 3D: Chronicles of Velen", borderless=False)
window.size = (1200, 750)
window.color = color.rgb(110, 145, 175) # Чистое небо без слепящего тумана

# =====================================================================
# НАСТРОЙКИ СВЕТА И СЦЕНЫ
# =====================================================================
sun = DirectionalLight(y=25, z=-20, shadows=True, rotation=(45, -45, 0))
ambient = AmbientLight(color=color.rgba(140, 140, 150, 200))

# Земля (Ландшафт Велена)
terrain = Entity(
    model='plane',
    scale=(300, 1, 300),
    color=color.rgb(55, 75, 45),
    texture='white_cube',
    texture_scale=(150, 150),
    collider='box'
)

# =====================================================================
# ДЕКОРАЦИИ И ПОСТРОЙКИ (ИЗБЫ, ДЕРЕВЬЯ, РУИНЫ)
# =====================================================================
class House3D(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        # Сруб
        self.walls = Entity(parent=self, model='cube', scale=(8, 4.5, 9), color=color.rgb(75, 50, 32), position=(0, 2.25, 0), collider='box')
        # Крыша
        self.roof = Entity(parent=self, model='cone', scale=(10, 3.8, 11), color=color.rgb(130, 105, 55), position=(0, 5.8, 0), rotation=(0, 45, 0))
        # Дверь
        self.door = Entity(parent=self, model='cube', scale=(1.8, 2.8, 0.2), color=color.rgb(40, 25, 15), position=(0, 1.4, -4.55))

class Tree3D(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        # Ствол
        self.trunk = Entity(parent=self, model='cylinder', scale=(0.7, 5, 0.7), color=color.rgb(55, 38, 25), position=(0, 2.5, 0), collider='box')
        # Листва (два яруса)
        self.leaves1 = Entity(parent=self, model='cone', scale=(4.5, 4.5, 4.5), color=color.rgb(28, 48, 28), position=(0, 5.5, 0))
        self.leaves2 = Entity(parent=self, model='cone', scale=(3.5, 4.0, 3.5), color=color.rgb(32, 55, 32), position=(0, 7.5, 0))

# Генерация деревни и леса
houses = [House3D(pos=(14, 0, 16)), House3D(pos=(-16, 0, 20)), House3D(pos=(18, 0, -18))]
for _ in range(40):
    tx = random.uniform(-110, 110)
    tz = random.uniform(-110, 110)
    if abs(tx) > 15 or abs(tz) > 15:
        Tree3D(pos=(tx, 0, tz))

# =====================================================================
# МОДЕЛЬ ГЕРАЛЬТА (3-Е ЛИЦО)
# =====================================================================
class WitcherPlayer(Entity):
    def __init__(self):
        super().__init__(position=(0, 0, 0))
        
        # Тело в доспехе Школы Волка
        self.torso = Entity(parent=self, model='cube', scale=(0.85, 1.35, 0.45), color=color.rgb(45, 40, 35), position=(0, 1.35, 0))
        self.vest = Entity(parent=self.torso, model='cube', scale=(1.02, 0.65, 1.05), color=color.rgb(75, 45, 30), position=(0, 0.1, 0))
        
        # Голова и седые волосы
        self.head = Entity(parent=self, model='sphere', scale=0.48, color=color.rgb(220, 185, 155), position=(0, 2.25, 0))
        self.hair = Entity(parent=self.head, model='sphere', scale=(1.08, 0.6, 1.1), color=color.rgb(240, 240, 245), position=(0, 0.25, -0.05))
        
        # Ноги
        self.leg_l = Entity(parent=self, model='cube', scale=(0.3, 0.8, 0.35), color=color.rgb(25, 20, 15), position=(-0.22, 0.4, 0))
        self.leg_r = Entity(parent=self, model='cube', scale=(0.3, 0.8, 0.35), color=color.rgb(25, 20, 15), position=(0.22, 0.4, 0))

        # Мечи за спиной (Стальной и Серебряный)
        self.sheath_steel = Entity(parent=self.torso, model='cube', scale=(0.06, 1.6, 0.06), color=color.gray, position=(-0.22, 0.2, -0.28), rotation=(0, 0, -25))
        self.sheath_silver = Entity(parent=self.torso, model='cube', scale=(0.06, 1.6, 0.06), color=color.cyan, position=(0.22, 0.2, -0.28), rotation=(0, 0, 25))

        # Боевой клинок в правой руке
        self.blade = Entity(parent=self, model='cube', scale=(0.06, 1.5, 0.06), color=color.white, position=(0.65, 1.2, 0.5), rotation=(80, 0, 0), enabled=False)

        # Сфера знака Квен
        self.quen_sphere = Entity(parent=self, model='sphere', scale=3.2, color=color.rgba(255, 215, 0, 80), position=(0, 1.5, 0), enabled=False)

        # Характеристики
        self.hp = 220
        self.max_hp = 220
        self.stamina = 100
        self.max_stamina = 100
        self.toxicity = 0
        self.speed = 8.5
        
        # Инвентарь
        self.coins = 150
        self.potions = 3
        self.herbs = 4
        self.sword = "SILVER" # "SILVER" или "STEEL"
        self.quen_active = False
        self.is_attacking = False
        self.dodge_timer = 0.0
        self.step = 0.0

    def attack(self):
        if not self.is_attacking and self.stamina >= 12:
            self.is_attacking = True
            self.stamina -= 12
            self.blade.color = color.cyan if self.sword == "SILVER" else color.rgb(220, 200, 160)
            self.blade.enabled = True
            self.blade.animate_rotation((15, 60, -45), duration=0.14)
            invoke(self.reset_blade, delay=0.22)

    def reset_blade(self):
        self.blade.rotation = (80, 0, 0)
        self.blade.enabled = False
        self.is_attacking = False

    def drink_potion(self):
        if self.potions > 0 and self.toxicity <= 80:
            self.potions -= 1
            self.hp = min(self.max_hp, self.hp + 65)
            self.toxicity += 30
            show_banner("+65 HP (Ласточка выпита)", color.green)
        elif self.toxicity > 80:
            show_banner("Слишком высокая токсичность!", color.red)

player = WitcherPlayer()

# =====================================================================
# ОРБИТАЛЬНАЯ КАМЕРА 3-ГО ЛИЦА
# =====================================================================
class OrbitCamera:
    def __init__(self, target):
        self.target = target
        self.dist = 7.0
        self.height = 2.4
        self.yaw = 0.0
        self.pitch = 18.0

    def update(self):
        # Вращение орбиты мышью
        self.yaw += mouse.velocity[0] * 120
        self.pitch = clamp(self.pitch - mouse.velocity[1] * 75, -5, 55)

        rad_y = math.radians(self.yaw)
        rad_p = math.radians(self.pitch)

        cx = self.target.x - self.dist * math.sin(rad_y) * math.cos(rad_p)
        cz = self.target.z - self.dist * math.cos(rad_y) * math.cos(rad_p)
        cy = self.target.y + self.height + self.dist * math.sin(rad_p)

        camera.position = Vec3(cx, cy, cz)
        camera.look_at(self.target.position + Vec3(0, 1.6, 0))

cam = OrbitCamera(player)
mouse.locked = True

# =====================================================================
# ВРАГИ: УТОПЕЦ И БОСС ЛЕШИЙ
# =====================================================================
class Drowner3D(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.body = Entity(parent=self, model='cube', scale=(0.9, 1.5, 0.7), color=color.rgb(40, 115, 125), position=(0, 0.75, 0), collider='box')
        # Красные светящиеся глаза
        Entity(parent=self.body, model='sphere', scale=0.14, color=color.red, position=(-0.25, 0.4, 0.38))
        Entity(parent=self.body, model='sphere', scale=0.14, color=color.red, position=(0.25, 0.4, 0.38))

        self.hp = 85
        self.max_hp = 85
        self.speed = 3.6
        self.attack_cd = 0.0
        self.slow_timer = 0.0
        self.stun_timer = 0.0
        self.alive = True

    def update(self):
        if not self.alive: return
        if self.stun_timer > 0:
            self.stun_timer -= time.dt
            return

        if self.slow_timer > 0: self.slow_timer -= time.dt
        spd = self.speed * 0.4 if self.slow_timer > 0 else self.speed

        self.attack_cd = max(0.0, self.attack_cd - time.dt)
        dist = distance(self.position, player.position)

        if 2.0 < dist < 28:
            self.look_at_2d(player.position)
            self.position += self.forward * spd * time.dt

        if dist <= 2.2 and self.attack_cd <= 0:
            self.attack_cd = 1.3
            if player.quen_active:
                player.quen_active = False
                player.quen_sphere.enabled = False
                show_banner("КВЕН ПОГЛОТИЛ УДАР!", color.yellow)
            else:
                player.hp -= 24
                show_banner("УРОН: -24 HP!", color.red)

class AncientLeshenBoss(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        # Массивное тело из древесной коры
        self.body = Entity(parent=self, model='cube', scale=(1.8, 3.4, 1.2), color=color.rgb(40, 26, 18), position=(0, 1.7, 0), collider='box')
        # Череп оленя и рога
        self.skull = Entity(parent=self, model='cone', scale=(1.2, 1.6, 1.0), color=color.rgb(220, 215, 190), position=(0, 3.4, 0.3), rotation=(90, 0, 0))
        Entity(parent=self.skull, model='cylinder', scale=(0.08, 2.2, 0.08), color=color.rgb(25, 18, 12), position=(-0.6, 0.8, 0), rotation=(0, 0, -40))
        Entity(parent=self.skull, model='cylinder', scale=(0.08, 2.2, 0.08), color=color.rgb(25, 18, 12), position=(0.6, 0.8, 0), rotation=(0, 0, 40))

        self.hp = 650
        self.max_hp = 650
        self.speed = 2.4
        self.teleport_cd = 8.0
        self.root_cd = 4.5
        self.slow_timer = 0.0
        self.alive = True

    def update(self):
        if not self.alive: return
        self.teleport_cd -= time.dt
        self.root_cd -= time.dt

        dist = distance(self.position, player.position)

        # Телепортация за спину игрока
        if self.teleport_cd <= 0 and dist < 35:
            self.teleport_cd = 9.0
            ang = random.uniform(0, 6.28)
            self.position = player.position + Vec3(math.cos(ang) * 9, 0, math.sin(ang) * 9)
            show_banner("Леший растворился в тумане!", color.smoke)

        # Вызов корней под ногами
        if self.root_cd <= 0 and dist < 30:
            self.root_cd = 5.5
            spawn_roots(player.position)
            show_banner("КОРНИ ЗЕМЛИ!", color.orange)

        if dist > 3.0:
            self.look_at_2d(player.position)
            self.position += self.forward * self.speed * time.dt

# Ловушки корней Лешего
active_roots = []
def spawn_roots(target_pos):
    root = Entity(model='cone', scale=(2.2, 3.5, 2.2), color=color.rgb(45, 65, 30), position=target_pos, collider='box')
    active_roots.append({"entity": root, "timer": 2.2})

# =====================================================================
# NPC: СТАРОСТА И ТРАВНИЦА
# =====================================================================
class NPC3D(Entity):
    def __init__(self, pos, name, role):
        super().__init__(position=pos)
        self.name = name
        self.role = role
        col = color.rgb(160, 140, 110) if role == "elder" else color.rgb(45, 115, 75)
        self.body = Entity(parent=self, model='cube', scale=(0.85, 1.4, 0.45), color=col, position=(0, 0.7, 0), collider='box')
        self.head = Entity(parent=self, model='sphere', scale=0.5, color=color.rgb(215, 185, 155), position=(0, 1.6, 0))
        # Восклицательный знак квеста
        self.quest_icon = Entity(parent=self, model='sphere', scale=0.25, color=color.yellow, position=(0, 2.2, 0))

npc_elder = NPC3D(pos=(10, 0, 12), name="Староста Богдан", role="elder")
npc_tomira = NPC3D(pos=(-12, 0, 16), name="Травница Томира", role="herbalist")

drowner = Drowner3D(pos=(35, 0, 30))
leshen = AncientLeshenBoss(pos=(80, 0, 75))

# =====================================================================
# СИСТЕМА КВЕСТОВ, ДИАЛОГОВ И БЕСТИАРИЙ
# =====================================================================
quest_data = {
    "leshen": {"name": "Заказ: Древний Хозяин Велена", "stage": 0, "desc": "Убейте Древнего Лешего в восточной глуши."},
    "herbs": {"name": "Сбор трав для Томиры", "stage": 0, "desc": "Принесите Томире 3 ласточкины травы."}
}

bestiary_text = (
    "1. УТОПЕЦ (Трупоед):\n"
    "Слабости: Серебряный меч, Знак Игни (горит), Знак Аард.\n\n"
    "2. ДРЕВНИЙ ЛЕШИЙ (Реликт):\n"
    "Слабости: Серебряный меч, Игни (крайне восприимчив к огню), Ирден (ловушка замедления).\n\n"
    "3. ДЕЗЕРТИРЫ (Люди):\n"
    "Слабости: Стальной меч, Знак Аксий (гипноз)."
)

# =====================================================================
# ИНТЕРФЕЙС / HUD И ОКНА МЕНЮ
# =====================================================================
hud_bg = Entity(parent=camera.ui, model='quad', scale=(0.55, 0.12), position=(-0.58, 0.42), color=color.rgba(20, 20, 20, 190))
hud_bars = Text(text="", position=(-0.83, 0.46), scale=1.1, color=color.white)

banner = Text(text="", origin=(0, 0), position=(0, 0.28), scale=1.6, color=color.yellow, enabled=False)
def show_banner(msg, col=color.yellow):
    banner.text = msg
    banner.color = col
    banner.enabled = True
    invoke(setattr, banner, 'enabled', False, delay=2.0)

# Диалоговое окно
dialog_box = WindowPanel(
    title='Диалог',
    content=(
        Text(text="", id='dialog_text'),
        Button(text='1. Принять заказ / Выполнить', color=color.azure, id='btn_1'),
        Button(text='2. Отказаться', color=color.gray, id='btn_2')
    ),
    enabled=False
)

# Окно Меню (Инвентарь / Бестиарий / Квесты)
menu_panel = WindowPanel(
    title='ВЕДЬМАЧИЙ ГРИМУАР',
    content=(
        Text(text="[1] Инвентарь   [2] Бестиарий   [3] Квесты", color=color.gold),
        Text(text="", id='menu_content'),
        Button(text='Закрыть [I / ESC]', color=color.dark_gray)
    ),
    enabled=False
)
menu_tab = "INV"

# =====================================================================
# ГЛАВНЫЙ ИГРОВОЙ ЦИКЛ
# =====================================================================
def update():
    # Регенерация выносливости
    if player.stamina < player.max_stamina:
        player.stamina = min(player.max_stamina, player.stamina + 22 * time.dt)

    # Спад токсичности
    if player.toxicity > 0:
        player.toxicity = max(0, player.toxicity - 2.5 * time.dt)

    # Обновление камеры
    if not menu_panel.enabled and not dialog_box.enabled:
        cam.update()

    # Перемещение персонажа относительно камеры
    if not menu_panel.enabled and not dialog_box.enabled:
        move_dir = Vec3(
            (held_keys['d'] - held_keys['a']),
            0,
            (held_keys['w'] - held_keys['s'])
        ).normalized()

        if move_dir.length() > 0:
            player.step += time.dt * 12
            rad = math.radians(cam.yaw)
            fwd = Vec3(math.sin(rad), 0, math.cos(rad))
            rgt = Vec3(math.cos(rad), 0, -math.sin(rad))
            world_dir = fwd * move_dir.z + rgt * move_dir.x

            spd = player.speed * 0.6 if mouse.right else player.speed
            player.position += world_dir * spd * time.dt
            player.rotation_y = math.degrees(math.atan2(world_dir.x, world_dir.z))
            
            # Анимация покачивания ног
            player.leg_l.rotation_x = math.sin(player.step) * 28
            player.leg_r.rotation_x = -math.sin(player.step) * 28
        else:
            player.leg_l.rotation_x = 0
            player.leg_r.rotation_x = 0

    # Ведьмачье чутье (ПКМ)
    if mouse.right and not menu_panel.enabled:
        window.color = color.rgb(30, 25, 20)
        drowner.body.color = color.red
        leshen.body.color = color.rgb(255, 80, 0)
    else:
        window.color = color.rgb(110, 145, 175)
        drowner.body.color = color.rgb(40, 115, 125)
        leshen.body.color = color.rgb(40, 26, 18)

    # Логика корней босса
    for r in active_roots[:]:
        r["timer"] -= time.dt
        if distance(r["entity"].position, player.position) < 2.0 and r["timer"] > 0:
            if player.quen_active:
                player.quen_active = False
                player.quen_sphere.enabled = False
                show_banner("КВЕН РАЗБИТ КОРНЯМИ!", color.yellow)
            else:
                player.hp -= 40 * time.dt
                show_banner("ШИПЫ ЛЕСА: -40 HP!", color.red)
        if r["timer"] <= 0:
            destroy(r["entity"])
            active_roots.remove(r)

    # Обновление HUD
    hud_bars.text = (
        f"HP: {int(player.hp)}/{player.max_hp}  |  ЭНЕРГИЯ: {int(player.stamina)}/{player.max_stamina}\n"
        f"ТОКСИЧНОСТЬ: {int(player.toxicity)}%  |  МЕЧ [R]: {player.sword}\n"
        f"КРОНЫ: {player.coins}  |  ЛАСТОЧКА [F]: {player.potions} шт."
    )

# =====================================================================
# ВВОД И УПРАВЛЕНИЕ БОЕМ / ДИАЛОГАМИ
# =====================================================================
def input(key):
    global menu_tab

    # Открытие меню [I]
    if key == 'i':
        menu_panel.enabled = not menu_panel.enabled
        mouse.locked = not menu_panel.enabled
        update_menu_content()

    if menu_panel.enabled:
        if key == '1': menu_tab = "INV"; update_menu_content()
        if key == '2': menu_tab = "BESTIARY"; update_menu_content()
        if key == '3': menu_tab = "QUESTS"; update_menu_content()
        if key == 'escape':
            menu_panel.enabled = False
            mouse.locked = True
        return

    # Диалоги
    if dialog_box.enabled:
        if key in ('1', 'e'):
            dialog_box.content[1].on_click()
        elif key in ('2', 'escape'):
            dialog_box.enabled = False
            mouse.locked = True
        return

    # Атака мечом [ЛКМ]
    if key == 'left mouse down':
        player.attack()
        # Проверка удара по утопцу
        if distance(player.position, drowner.position) < 3.2 and drowner.alive:
            mult = 1.0 if player.sword == "SILVER" else 0.25
            dmg = int(35 * mult)
            drowner.hp -= dmg
            show_banner(f"УДАР МЕЧОМ: -{dmg} HP!" if mult == 1.0 else f"-{dmg} HP (Нужно Серебро!)", color.white)
            if drowner.hp <= 0:
                drowner.alive = False
                drowner.body.rotation_x = 90
                drowner.y = 0.2
                show_banner("УТОПЕЦ ПОВЕРЖЕН! +18 Крон", color.gold)
                player.coins += 18

        # Проверка удара по Лешему
        if distance(player.position, leshen.position) < 4.2 and leshen.alive:
            mult = 1.0 if player.sword == "SILVER" else 0.2
            dmg = int(45 * mult)
            leshen.hp -= dmg
            show_banner(f"УДАР ПО ЛЕШЕМУ: -{dmg} HP!", color.gold)
            if leshen.hp <= 0:
                leshen.alive = False
                leshen.body.rotation_x = 90
                leshen.y = 0.4
                quest_data["leshen"]["stage"] = 2
                show_banner("ЛЕШИЙ ПОВЕРЖЕН! Вернитесь к Старосте.", color.gold)

    # Смена меча [R]
    if key == 'r':
        player.sword = "STEEL" if player.sword == "SILVER" else "SILVER"
        show_banner(f"Обнажен {player.sword} меч", color.cyan if player.sword == "SILVER" else color.orange)

    # Эликсир Ласточка [F]
    if key == 'f':
        player.drink_potion()

    # --- ЗНАКИ ВЕДЬМАКА ---
    # 1. ИГНИ (Струя огня)
    if key == '1' and player.stamina >= 35:
        player.stamina -= 35
        for i in range(12):
            f = Entity(model='sphere', color=color.orange, scale=0.45, position=player.position + Vec3(0, 1.4, 0) + player.forward * 1.2)
            f.animate_position(f.position + (player.forward + Vec3(random.uniform(-0.3, 0.3), 0, random.uniform(-0.3, 0.3))) * 11, duration=0.4)
            destroy(f, delay=0.45)
        if distance(player.position, leshen.position) < 11 and leshen.alive:
            leshen.hp -= 60
            show_banner("ИГНИ: ОЖОГ ЛЕШЕГО -60 HP!", color.orange)
        if distance(player.position, drowner.position) < 10 and drowner.alive:
            drowner.hp -= 40
            show_banner("ИГНИ: УТОПЕЦ ГОРИТ -40 HP!", color.orange)

    # 2. КВЕН (Щит)
    if key == '2' and player.stamina >= 40:
        player.stamina -= 40
        player.quen_active = True
        player.quen_sphere.enabled = True
        show_banner("ЗНАК КВЕН АКТИВЕН", color.yellow)

    # 3. ААРД (Телекинез)
    if key == '3' and player.stamina >= 35:
        player.stamina -= 35
        wave = Entity(model='cone', scale=(4, 1, 4), color=color.cyan, position=player.position + Vec3(0, 1, 0), rotation=(90, player.rotation_y, 0))
        wave.animate_position(wave.position + player.forward * 8, duration=0.3)
        destroy(wave, delay=0.35)
        if distance(player.position, drowner.position) < 8 and drowner.alive:
            drowner.position += player.forward * 4.5
            drowner.hp -= 20
            drowner.stun_timer = 2.0
            show_banner("ААРД: СТАН И ОТБРАСЫВАНИЕ!", color.cyan)

    # 4. ИРДЕН (Ловушка)
    if key == '4' and player.stamina >= 35:
        player.stamina -= 35
        trap = Entity(model='circle', scale=8, color=color.rgba(180, 50, 240, 100), position=player.position + Vec3(0, 0.05, 0), rotation=(90, 0, 0))
        destroy(trap, delay=6.0)
        show_banner("ИРДЕН: ЗОНА ЗАМЕДЛЕНИЯ", color.magenta)

    # 5. АКСИЙ (Гипноз)
    if key == '5' and player.stamina >= 35:
        player.stamina -= 35
        if distance(player.position, drowner.position) < 9 and drowner.alive:
            drowner.stun_timer = 4.0
            show_banner("АКСИЙ: УТОПЕЦ ОДУРМАНЕН!", color.green)

    # Взаимодействие с NPC [E]
    if key == 'e':
        if distance(player.position, npc_elder.position) < 3.8:
            open_elder_dialog()
        elif distance(player.position, npc_tomira.position) < 3.8:
            open_tomira_dialog()

def open_elder_dialog():
    mouse.locked = False
    dialog_box.enabled = True
    st = quest_data["leshen"]["stage"]
    if st == 0:
        dialog_box.content[0].text = "Ведьмак! В восточной чаще завёлся древний Леший.\nУбей его, и 250 крон твои!"
        def accept():
            quest_data["leshen"]["stage"] = 1
            dialog_box.enabled = False
            mouse.locked = True
            show_banner("ЗАКАЗ ВЗЯТ: Найдите Лешего на востоке!", color.gold)
        dialog_box.content[1].text = "1. Беру заказ [250 крон]"
        dialog_box.content[1].on_click = accept
    elif st == 2:
        dialog_box.content[0].text = "Слава богам, ты сразил чудовище!\nВот твоя награда, Белый Волк."
        def reward():
            quest_data["leshen"]["stage"] = 3
            player.coins += 250
            npc_elder.quest_icon.enabled = False
            dialog_box.enabled = False
            mouse.locked = True
            show_banner("ЗАКАЗ ВЫПОЛНЕН! +250 КРОН", color.gold)
        dialog_box.content[1].text = "1. Забрать награду (250 крон)"
        dialog_box.content[1].on_click = reward
    else:
        dialog_box.content[0].text = "Ступай на восток, включи чутье и ищи логово Лешего!"
        def close():
            dialog_box.enabled = False
            mouse.locked = True
        dialog_box.content[1].text = "1. Уже иду."
        dialog_box.content[1].on_click = close

def open_tomira_dialog():
    mouse.locked = False
    dialog_box.enabled = True
    dialog_box.content[0].text = "Здравствуй, Геральт. Могу сварить 'Ласточку' за 40 крон."
    def buy():
        if player.coins >= 40:
            player.coins -= 40
            player.potions += 1
            show_banner("+1 Ласточка куплена", color.green)
        else:
            show_banner("Недостаточно крон!", color.red)
        dialog_box.enabled = False
        mouse.locked = True
    dialog_box.content[1].text = "1. Купить 'Ласточку' (-40 крон)"
    dialog_box.content[1].on_click = buy

def update_menu_content():
    if menu_tab == "INV":
        menu_panel.content[1].text = (
            f"• Кроны: {player.coins} золотых\n"
            f"• Эликсиры 'Ласточка': {player.potions} шт. [Клавиша F]\n"
            f"• Травы для алхимии: {player.herbs} шт.\n"
            f"• Серебряный клинок Волка (+45 по монстрам)\n"
            f"• Стальной клинок Новиграда (+35 по людям)"
        )
    elif menu_tab == "BESTIARY":
        menu_panel.content[1].text = bestiary_text
    elif menu_tab == "QUESTS":
        q_lines = []
        for qid, q in quest_data.items():
            status = "ВЫПОЛНЕН" if q["stage"] == 3 else ("В ПРОЦЕССЕ" if q["stage"] > 0 else "НЕ ВЗЯТ")
            q_lines.append(f"• {q['name']} — [{status}]\n  {q['desc']}")
        menu_panel.content[1].text = "\n\n".join(q_lines)

app.run()