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


from ursina import *
from ursina.prefabs.health_bar import HealthBar
import math
import random
import time

# =====================================================================
# 1. ИНИЦИАЛИЗАЦИЯ ИГРОВОГО ДВИЖКА И ОКНА
# =====================================================================
app = Ursina(
    title="The Witcher 3D: Chronicles of the Continent",
    borderless=False,
    fullscreen=False
)
window.size = (1280, 760)
window.color = color.rgb(115, 145, 175)
window.fps_counter.enabled = True

# =====================================================================
# 2. ОСВЕЩЕНИЕ И ОКРУЖАЮЩАЯ СРЕДА
# =====================================================================
sun_light = DirectionalLight(y=35, z=-25, shadows=False, rotation=(45, -40, 0))
ambient_light = AmbientLight(color=color.rgba(160, 160, 175, 255))

# Огромная карта Велена
world_terrain = Entity(
    model='plane',
    scale=(400, 1, 400),
    color=color.rgb(50, 70, 40),
    texture='white_cube',
    texture_scale=(200, 200),
    collider='box'
)

# =====================================================================
# 3. ВСПОМОГАТЕЛЬНЫЕ КЛАССЫ И ВИЗУАЛЬНЫЕ ЭФФЕКТЫ
# =====================================================================
class FloatingCombatText(Entity):
    def __init__(self, pos, text_str, text_col=color.white):
        super().__init__(
            parent=scene,
            position=pos + Vec3(random.uniform(-0.3, 0.3), 1.8, random.uniform(-0.3, 0.3))
        )
        self.label = Text(
            parent=self,
            text=text_str,
            color=text_col,
            scale=8,
            billboard=True,
            origin=(0, 0)
        )
        self.lifetime = 1.0
        self.speed = 1.5

    def update(self):
        self.y += self.speed * time.dt
        self.lifetime -= time.dt
        if self.lifetime <= 0:
            destroy(self)

class FireProjectile(Entity):
    def __init__(self, start_pos, dir_vector):
        super().__init__(
            model='sphere',
            scale=0.5,
            color=color.orange,
            position=start_pos,
            collider='sphere'
        )
        self.direction = dir_vector
        self.speed = 22.0
        self.lifetime = 0.55

    def update(self):
        self.position += self.direction * self.speed * time.dt
        self.scale += Vec3(1.2, 1.2, 1.2) * time.dt
        self.lifetime -= time.dt
        if self.lifetime <= 0:
            destroy(self)

class YrdenTrapEntity(Entity):
    def __init__(self, pos):
        super().__init__(
            model='circle',
            scale=9.0,
            color=color.rgba(180, 50, 240, 90),
            position=pos + Vec3(0, 0.08, 0),
            rotation=(90, 0, 0)
        )
        self.lifetime = 8.0

    def update(self):
        self.lifetime -= time.dt
        if self.lifetime <= 0:
            destroy(self)

# =====================================================================
# 4. ДЕКОРАЦИИ И АРХИТЕКТУРА МИРА
# =====================================================================
class VillageHouse(Entity):
    def __init__(self, pos, rot_y=0):
        super().__init__(position=pos, rotation_y=rot_y)
        # Сруб
        self.base = Entity(parent=self, model='cube', scale=(9, 5, 10), color=color.rgb(75, 48, 30), position=(0, 2.5, 0), collider='box')
        # Соломенная крыша
        self.roof = Entity(parent=self, model='cone', scale=(11, 4.2, 12), color=color.rgb(125, 95, 50), position=(0, 6.6, 0), rotation=(0, 45, 0))
        # Дверь
        self.door = Entity(parent=self, model='cube', scale=(2.0, 3.2, 0.2), color=color.rgb(40, 22, 14), position=(0, 1.6, -5.05))

class ForestTree(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.trunk = Entity(parent=self, model='cylinder', scale=(0.7, 6, 0.7), color=color.rgb(55, 36, 22), position=(0, 3, 0), collider='box')
        self.crown1 = Entity(parent=self, model='cone', scale=(5.0, 5.0, 5.0), color=color.rgb(25, 48, 25), position=(0, 6.5, 0))
        self.crown2 = Entity(parent=self, model='cone', scale=(3.8, 4.5, 3.8), color=color.rgb(32, 58, 32), position=(0, 8.8, 0))

class AncientMonolith(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.pillar = Entity(parent=self, model='cube', scale=(2.4, 8.0, 2.4), color=color.rgb(70, 75, 75), position=(0, 4.0, 0), collider='box')
        self.cap = Entity(parent=self, model='cone', scale=(3.2, 2.0, 3.2), color=color.rgb(50, 55, 55), position=(0, 8.8, 0))

# Генерация деревни Залипье
village_houses = [
    VillageHouse(pos=(18, 0, 20), rot_y=15),
    VillageHouse(pos=(-20, 0, 25), rot_y=-30),
    VillageHouse(pos=(22, 0, -22), rot_y=120)
]

# Генерация капища Лешего
monoliths = [
    AncientMonolith(pos=(85, 0, 85)),
    AncientMonolith(pos=(75, 0, 95)),
    AncientMonolith(pos=(95, 0, 75))
]

# Рассадка лесного массива
random.seed(1337)
for _ in range(65):
    tx = random.uniform(-160, 160)
    tz = random.uniform(-160, 160)
    if abs(tx) > 20 or abs(tz) > 20:
        ForestTree(pos=(tx, 0, tz))

# =====================================================================
# 5. ИНТЕРАКТИВНЫЕ ОБЪЕКТЫ (СУНДУКИ, ТРАВЫ)
# =====================================================================
class LootChest3D(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.box = Entity(parent=self, model='cube', scale=(1.4, 0.9, 0.9), color=color.rgb(110, 70, 35), position=(0, 0.45, 0), collider='box')
        self.lock = Entity(parent=self.box, model='cube', scale=(0.15, 0.2, 0.15), color=color.gold, position=(0, 0, 0.52))
        self.looted = False

    def loot(self, player_ref):
        if not self.looted:
            self.looted = True
            gold_gain = random.randint(35, 75)
            player_ref.coins += gold_gain
            player_ref.potions += 1
            FloatingCombatText(self.position, f"+{gold_gain} Крон | +1 Ласточка", color.gold)
            self.box.color = color.gray

class HerbPatch3D(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.stem = Entity(parent=self, model='sphere', scale=(0.7, 0.4, 0.7), color=color.rgb(40, 180, 70), position=(0, 0.2, 0), collider='box')
        self.flower = Entity(parent=self, model='sphere', scale=0.35, color=color.rgb(235, 245, 90), position=(0, 0.4, 0))
        self.picked = False

    def harvest(self, player_ref):
        if not self.picked:
            self.picked = True
            player_ref.herbs += 1
            FloatingCombatText(self.position, "+1 Ласточкина трава", color.green)
            self.flower.enabled = False

chests_in_world = [
    LootChest3D(pos=(24, 0, 24)),
    LootChest3D(pos=(-25, 0, 30)),
    LootChest3D(pos=(90, 0, 90))
]

herbs_in_world = [
    HerbPatch3D(pos=(8, 0, 15)),
    HerbPatch3D(pos=(-12, 0, 8)),
    HerbPatch3D(pos=(30, 0, -10)),
    HerbPatch3D(pos=(60, 0, 50)),
    HerbPatch3D(pos=(75, 0, 65))
]

# =====================================================================
# 6. ГЕРАЛЬТ ИЗ РИВИИ (ИГРОК 3-ГО ЛИЦА)
# =====================================================================
class WitcherPlayer(Entity):
    def __init__(self):
        super().__init__(position=(0, 0, 0))
        
        # Корневой контейнер визуала
        self.visual = Entity(parent=self, position=(0, 0, 0))

        # Тело (Доспех Школы Волка)
        self.torso = Entity(parent=self.visual, model='cube', scale=(0.9, 1.4, 0.5), 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(80, 48, 30), position=(0, 0.1, 0))
        self.medallion = Entity(parent=self.torso, model='sphere', scale=0.15, color=color.white, position=(0, 0.35, 0.55))

        # Голова и седые волосы
        self.head = Entity(parent=self.visual, model='sphere', scale=0.52, color=color.rgb(225, 190, 160), position=(0, 2.3, 0))
        self.hair = Entity(parent=self.head, model='sphere', scale=(1.08, 0.55, 1.1), color=color.rgb(245, 245, 250), position=(0, 0.25, -0.05))

        # Ноги для анимации бега
        self.leg_left = Entity(parent=self.visual, model='cube', scale=(0.32, 0.85, 0.35), color=color.rgb(24, 20, 16), position=(-0.24, 0.42, 0))
        self.leg_right = Entity(parent=self.visual, model='cube', scale=(0.32, 0.85, 0.35), color=color.rgb(24, 20, 16), position=(0.24, 0.42, 0))

        # Два меча за спиной
        self.sheath_steel = Entity(parent=self.torso, model='cube', scale=(0.06, 1.6, 0.06), color=color.gray, position=(-0.24, 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.24, 0.2, -0.28), rotation=(0, 0, 25))

        # Боевой клинок в руке
        self.hand_blade = Entity(parent=self.visual, model='cube', scale=(0.06, 1.6, 0.06), color=color.cyan, position=(0.68, 1.25, 0.45), rotation=(75, 0, 0), enabled=False)

        # Сфера защитного Знака Квен
        self.quen_sphere = Entity(parent=self, model='sphere', scale=3.3, color=color.rgba(255, 215, 0, 80), position=(0, 1.4, 0), enabled=False)

        # Характеристики
        self.hp = 250
        self.max_hp = 250
        self.stamina = 100
        self.max_stamina = 100
        self.toxicity = 0
        self.speed = 8.5
        
        # Инвентарь и боевой режим
        self.coins = 180
        self.potions = 3
        self.herbs = 2
        self.sword = "SILVER" # "SILVER" или "STEEL"
        self.is_attacking = False
        self.quen_active = False
        self.dodge_active = False
        self.anim_step = 0.0

    def attack(self):
        if not self.is_attacking and self.stamina >= 14:
            self.is_attacking = True
            self.stamina -= 14
            self.hand_blade.color = color.cyan if self.sword == "SILVER" else color.rgb(230, 210, 170)
            self.hand_blade.enabled = True
            self.hand_blade.animate_rotation((15, 65, -45), duration=0.12)
            invoke(self.reset_attack, delay=0.22)

    def reset_attack(self):
        self.hand_blade.rotation = (75, 0, 0)
        self.hand_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 + 70)
            self.toxicity += 30
            FloatingCombatText(self.position, "+70 HP (Ласточка)", color.green)
        elif self.toxicity > 80:
            FloatingCombatText(self.position, "Интоксикация критическая!", color.red)

player = WitcherPlayer()

# =====================================================================
# 7. КАМЕРА ТРЕТЬЕГО ЛИЦА С АБСОЛЮТНОЙ СТАБИЛИЗАЦИЕЙ
# =====================================================================
class WitcherCamera:
    def __init__(self, target_entity):
        self.target = target_entity
        self.dist = 6.8
        self.height = 2.4
        self.yaw = 0.0
        self.pitch = 18.0

    def update(self):
        self.yaw += mouse.velocity[0] * 125
        self.pitch = clamp(self.pitch - mouse.velocity[1] * 80, -8, 52)

        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.5, 0))

cam = WitcherCamera(player)
mouse.locked = True

# =====================================================================
# 8. ВРАГИ: УТОПЦЫ И ДРЕВНИЙ ЛЕШИЙ (БОСС)
# =====================================================================
class DrownerEntity(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.body = Entity(parent=self, model='cube', scale=(0.95, 1.5, 0.75), 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.38, 0.4))
        Entity(parent=self.body, model='sphere', scale=0.14, color=color.red, position=(0.25, 0.38, 0.4))

        self.hp = 90
        self.max_hp = 90
        self.speed = 3.6
        self.attack_cd = 0.0
        self.stun_timer = 0.0
        self.slow_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.35 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 < 26.0:
            self.look_at_2d(player.position)
            self.position += self.forward * spd * time.dt

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

class AncientLeshenBoss(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.body = Entity(parent=self, model='cube', scale=(2.0, 3.6, 1.4), color=color.rgb(42, 28, 20), position=(0, 1.8, 0), collider='box')
        self.skull = Entity(parent=self, model='cone', scale=(1.4, 1.8, 1.1), color=color.rgb(225, 220, 195), position=(0, 3.6, 0.4), rotation=(90, 0, 0))
        # Рога
        Entity(parent=self.skull, model='cylinder', scale=(0.1, 2.4, 0.1), color=color.rgb(28, 20, 14), position=(-0.7, 0.9, 0), rotation=(0, 0, -42))
        Entity(parent=self.skull, model='cylinder', scale=(0.1, 2.4, 0.1), color=color.rgb(28, 20, 14), position=(0.7, 0.9, 0), rotation=(0, 0, 42))

        self.hp = 750
        self.max_hp = 750
        self.speed = 2.4
        self.teleport_cd = 7.5
        self.root_cd = 4.0
        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 < 38.0:
            self.teleport_cd = 8.5
            ang = random.uniform(0, 6.28)
            self.position = player.position + Vec3(math.cos(ang) * 9.5, 0, math.sin(ang) * 9.5)
            FloatingCombatText(self.position, "*Растворился в тумане*", color.smoke)

        # Корни из-под земли
        if self.root_cd <= 0 and dist < 32.0:
            self.root_cd = 5.0
            spawn_root_trap(player.position)
            FloatingCombatText(self.position, "ГНЕВ ЗЕМЛИ!", color.orange)

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

active_roots = []
def spawn_root_trap(pos):
    root = Entity(model='cone', scale=(2.4, 3.6, 2.4), color=color.rgb(45, 65, 30), position=pos, collider='box')
    active_roots.append({"entity": root, "timer": 2.2})

drowners_list = [
    DrownerEntity(pos=(32, 0, 30)),
    DrownerEntity(pos=(38, 0, 36)),
    DrownerEntity(pos=(-40, 0, 40))
]
leshen_boss = AncientLeshenBoss(pos=(85, 0, 85))

# =====================================================================
# 9. СЮЖЕТНЫЕ NPC (СТАРОСТА, ТРАВНИЦА, БАРОН)
# =====================================================================
class StoryNPC(Entity):
    def __init__(self, pos, name, role):
        super().__init__(position=pos)
        self.name = name
        self.role = role
        col = color.rgb(165, 140, 110) if role == "elder" else (color.rgb(45, 120, 75) if role == "herbalist" else color.rgb(120, 35, 45))
        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 = StoryNPC(pos=(14, 0, 15), name="Староста Богдан", role="elder")
npc_tomira = StoryNPC(pos=(-16, 0, 20), name="Травница Томира", role="herbalist")
npc_baron = StoryNPC(pos=(18, 0, -16), name="Кровавый Барон", role="baron")

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

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

# =====================================================================
# 11. ИНТЕРФЕЙС, ДИАЛОГОВЫЕ ОКНА И МЕНЮ
# =====================================================================
hud_bg = Entity(parent=camera.ui, model='quad', scale=(0.58, 0.14), position=(-0.58, 0.42), color=color.rgba(20, 20, 20, 200))
hud_text = Text(text="", position=(-0.84, 0.47), scale=1.1, color=color.white)

dialog_window = WindowPanel(
    title='Диалог',
    content=(
        Text(text="", id='d_text'),
        Button(text='1. Согласиться / Выполнить', color=color.azure),
        Button(text='2. Отказаться', color=color.gray)
    ),
    enabled=False
)

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

def refresh_menu_content():
    if menu_current_tab == "INV":
        menu_window.content[1].text = (
            f"• Золотые кроны: {player.coins}\n"
            f"• Эликсир 'Ласточка': {player.potions} шт. [F]\n"
            f"• Ласточкина трава: {player.herbs} шт.\n"
            f"• Серебряный меч Волка (Урон +45 по чудовищам)\n"
            f"• Стальной меч Новиграда (Урон +35 по людям)"
        )
    elif menu_current_tab == "BESTIARY":
        menu_window.content[1].text = bestiary_text
    elif menu_current_tab == "QUESTS":
        lines = []
        for qid, q in quest_database.items():
            st_str = "ВЫПОЛНЕН" if q["stage"] == 3 else ("АКТИВЕН" if q["stage"] > 0 else "НЕ НАЧАТ")
            lines.append(f"• {q['name']} — [{st_str}]\n  {q['desc']}")
        menu_window.content[1].text = "\n\n".join(lines)

# =====================================================================
# 12. ГЛАВНЫЙ ЦИКЛ ОБНОВЛЕНИЯ (UPDATE)
# =====================================================================
def update():
    # Регенерация энергии
    if player.stamina < player.max_stamina:
        player.stamina = min(player.max_stamina, player.stamina + 24 * time.dt)

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

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

    # Перемещение Геральта
    if not menu_window.enabled and not dialog_window.enabled:
        move_vec = Vec3(
            (held_keys['d'] - held_keys['a']),
            0,
            (held_keys['w'] - held_keys['s'])
        ).normalized()

        if move_vec.length() > 0:
            player.anim_step += time.dt * 12
            rad_y = math.radians(cam.yaw)
            fwd = Vec3(math.sin(rad_y), 0, math.cos(rad_y))
            rgt = Vec3(math.cos(rad_y), 0, -math.sin(rad_y))
            world_move = (fwd * move_vec.z + rgt * move_vec.x).normalized()

            spd = player.speed * 0.6 if mouse.right else player.speed
            player.position += world_move * spd * time.dt
            player.visual.rotation_y = math.degrees(math.atan2(world_move.x, world_move.z))

            player.leg_left.rotation_x = math.sin(player.anim_step) * 28
            player.leg_right.rotation_x = -math.sin(player.anim_step) * 28
        else:
            player.leg_left.rotation_x = 0
            player.leg_right.rotation_x = 0

    # Ведьмачье чутье (ПКМ)
    if mouse.right and not menu_window.enabled:
        window.color = color.rgb(28, 24, 20)
        for d in drowners_list:
            if d.alive: d.body.color = color.red
        if leshen_boss.alive: leshen_boss.body.color = color.rgb(255, 75, 0)
    else:
        window.color = color.rgb(115, 145, 175)
        for d in drowners_list:
            if d.alive: d.body.color = color.rgb(40, 115, 125)
        if leshen_boss.alive: leshen_boss.body.color = color.rgb(42, 28, 20)

    # Проверка урона от корней Лешего
    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
                FloatingCombatText(player.position, "КВЕН РАЗБИТ КОРНЯМИ!", color.yellow)
            else:
                player.hp -= 38 * time.dt
                FloatingCombatText(player.position, "-38 HP (Шипы)!", color.red)
        if r["timer"] <= 0:
            destroy(r["entity"])
            active_roots.remove(r)

    # Обновление HUD
    hud_text.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}  |  ТРАВЫ: {player.herbs}"
    )

# =====================================================================
# 13. ОБРАБОТКА ВВОДА, БОЯ И ДИАЛОГОВ (INPUT)
# =====================================================================
def input(key):
    global menu_current_tab

    # Открытие Гримуара [I]
    if key == 'i':
        menu_window.enabled = not menu_window.enabled
        mouse.locked = not menu_window.enabled
        refresh_menu_content()

    if menu_window.enabled:
        if key == '1': menu_current_tab = "INV"; refresh_menu_content()
        if key == '2': menu_current_tab = "BESTIARY"; refresh_menu_content()
        if key == '3': menu_current_tab = "QUESTS"; refresh_menu_content()
        if key == 'escape':
            menu_window.enabled = False
            mouse.locked = True
        return

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

    # Атака клинком [ЛКМ]
    if key == 'left mouse down':
        player.attack()
        
        # Урон по Утопцам
        for d in drowners_list:
            if d.alive and distance(player.position, d.position) < 3.2:
                mult = 1.0 if player.sword == "SILVER" else 0.25
                dmg = int(38 * mult)
                d.hp -= dmg
                col = color.white if mult == 1.0 else color.gray
                FloatingCombatText(d.position, f"-{dmg} HP" if mult == 1.0 else f"-{dmg} HP (Смени меч [R]!)", col)
                if d.hp <= 0:
                    d.alive = False
                    d.body.rotation_x = 90
                    d.y = 0.2
                    player.coins += 20
                    FloatingCombatText(d.position, "УТОПЕЦ УБИТ! +20 Крон", color.gold)

        # Урон по Лешему
        if leshen_boss.alive and distance(player.position, leshen_boss.position) < 4.2:
            mult = 1.0 if player.sword == "SILVER" else 0.2
            dmg = int(48 * mult)
            leshen_boss.hp -= dmg
            FloatingCombatText(leshen_boss.position, f"-{dmg} HP", color.gold)
            if leshen_boss.hp <= 0:
                leshen_boss.alive = False
                leshen_boss.body.rotation_x = 90
                leshen_boss.y = 0.3
                quest_database["leshen"]["stage"] = 2
                FloatingCombatText(leshen_boss.position, "ЛЕШИЙ ПОВЕРЖЕН!", color.gold)

    # Переключение мечей [R]
    if key == 'r':
        player.sword = "STEEL" if player.sword == "SILVER" else "SILVER"
        col = color.cyan if player.sword == "SILVER" else color.rgb(230, 210, 170)
        FloatingCombatText(player.position, f"Обнажен {player.sword} меч", col)

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

    # --- 5 ЗНАКОВ ВЕДЬМАКА ---
    # 1. ИГНИ
    if key == '1' and player.stamina >= 35:
        player.stamina -= 35
        FloatingCombatText(player.position, "ЗНАК ИГНИ!", color.orange)
        for _ in range(8):
            spread = Vec3(random.uniform(-0.25, 0.25), 0, random.uniform(-0.25, 0.25))
            FireProjectile(player.position + Vec3(0, 1.4, 0), (player.forward + spread).normalized())
        
        for d in drowners_list:
            if d.alive and distance(player.position, d.position) < 9.0:
                d.hp -= 45
                FloatingCombatText(d.position, "ОЖОГ ИГНИ: -45 HP!", color.orange)
        if leshen_boss.alive and distance(player.position, leshen_boss.position) < 11.0:
            leshen_boss.hp -= 70
            FloatingCombatText(leshen_boss.position, "ЛЕШИЙ ГОРИТ: -70 HP!", color.orange)

    # 2. КВЕН
    if key == '2' and player.stamina >= 40:
        player.stamina -= 40
        player.quen_active = True
        player.quen_sphere.enabled = True
        FloatingCombatText(player.position, "ЗНАК КВЕН!", color.yellow)

    # 3. ААРД
    if key == '3' and player.stamina >= 35:
        player.stamina -= 35
        FloatingCombatText(player.position, "ЗНАК ААРД!", color.cyan)
        for d in drowners_list:
            if d.alive and distance(player.position, d.position) < 8.0:
                d.position += player.forward * 4.5
                d.hp -= 20
                d.stun_timer = 2.0
                FloatingCombatText(d.position, "ААРД: СТАН!", color.cyan)

    # 4. ИРДЕН
    if key == '4' and player.stamina >= 35:
        player.stamina -= 35
        YrdenTrapEntity(player.position)
        FloatingCombatText(player.position, "ЗНАК ИРДЕН!", color.magenta)
        for d in drowners_list:
            if distance(player.position, d.position) < 9.0:
                d.slow_timer = 6.0
        if distance(player.position, leshen_boss.position) < 9.0:
            leshen_boss.slow_timer = 6.0

    # 5. АКСИЙ
    if key == '5' and player.stamina >= 35:
        player.stamina -= 35
        FloatingCombatText(player.position, "ЗНАК АКСИЙ!", color.green)
        for d in drowners_list:
            if d.alive and distance(player.position, d.position) < 9.0:
                d.stun_timer = 4.5
                FloatingCombatText(d.position, "ОДУРМАНЕН!", color.green)

    # Взаимодействие [E]
    if key == 'e':
        # Сбор трав и сундуков
        for ch in chests_in_world:
            if not ch.looted and distance(player.position, ch.position) < 2.5:
                ch.loot(player)
        for hb in herbs_in_world:
            if not hb.picked and distance(player.position, hb.position) < 2.5:
                hb.harvest(player)

        # Диалоги с NPC
        if distance(player.position, npc_elder.position) < 3.8:
            open_npc_dialog(npc_elder)
        elif distance(player.position, npc_tomira.position) < 3.8:
            open_npc_dialog(npc_tomira)
        elif distance(player.position, npc_baron.position) < 3.8:
            open_npc_dialog(npc_baron)

def open_npc_dialog(npc):
    mouse.locked = False
    dialog_window.enabled = True

    if npc.role == "elder":
        st = quest_database["leshen"]["stage"]
        if st == 0:
            dialog_window.content[0].text = "Староста: Ведьмак, в восточной чаще объявился Леший!\nУбей его, и 300 крон твои."
            def accept():
                quest_database["leshen"]["stage"] = 1
                dialog_window.enabled = False
                mouse.locked = True
                FloatingCombatText(player.position, "ЗАКАЗ ВЗЯТ!", color.gold)
            dialog_window.content[1].text = "1. Беру заказ на чудище."
            dialog_window.content[1].on_click = accept
        elif st == 2:
            dialog_window.content[0].text = "Староста: Хвала богам, ты сразил тварь!\nВот твоя награда."
            def finish():
                quest_database["leshen"]["stage"] = 3
                player.coins += 300
                npc.quest_icon.enabled = False
                dialog_window.enabled = False
                mouse.locked = True
                FloatingCombatText(player.position, "+300 КРОН!", color.gold)
            dialog_window.content[1].text = "1. Забрать награду (300 крон)."
            dialog_window.content[1].on_click = finish
        else:
            dialog_window.content[0].text = "Староста: Ступай на восток к капищам и сокруши Лешего!"
            def close():
                dialog_window.enabled = False
                mouse.locked = True
            dialog_window.content[1].text = "1. Уже иду."
            dialog_window.content[1].on_click = close

    elif npc.role == "herbalist":
        dialog_window.content[0].text = "Томира: Здравствуй, Геральт. Могу сварить 'Ласточку' за 40 крон."
        def buy():
            if player.coins >= 40:
                player.coins -= 40
                player.potions += 1
                FloatingCombatText(player.position, "+1 Ласточка куплена", color.green)
            dialog_window.enabled = False
            mouse.locked = True
        dialog_window.content[1].text = "1. Купить 'Ласточку' (-40 крон)"
        dialog_window.content[1].on_click = buy

    elif npc.role == "baron":
        dialog_window.content[0].text = "Барон: Ведьмак, держи руку на рукояти меча. Время нынче темное."
        def close():
            dialog_window.enabled = False
            mouse.locked = True
        dialog_window.content[1].text = "1. Бывай, Барон."
        dialog_window.content[1].on_click = close

# =====================================================================
# 14. ЗАПУСК ИГРОВОГО ДВИЖКА
# =====================================================================
app.run()