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


from ursina import *
import math
import random
import time

# =====================================================================
# 1. ИНИЦИАЛИЗАЦИЯ И РЕШЕНИЕ ПРОБЛЕМЫ С БЕЛЫМ ЭКРАНОМ
# =====================================================================
app = Ursina(
    title="The Witcher 3D: Fixed Engine",
    borderless=False,
    fullscreen=False
)
window.size = (1280, 760)

# Принудительная настройка дальности и ближней плоскости камеры
camera.clip_plane_near = 0.1
camera.clip_plane_far = 600.0
window.color = color.rgb(85, 120, 160) # Чистое атмосферное небо

# =====================================================================
# 2. ОСВЕЩЕНИЕ И НАДЕЖНАЯ ГЕОМЕТРИЯ МИРА
# =====================================================================
sun = DirectionalLight(y=40, z=-30, shadows=False, rotation=(45, -35, 0))
ambient = AmbientLight(color=color.rgba(170, 175, 185, 255))

# Земля в виде объемного блока (никогда не исчезнет из кадра)
ground_terrain = Entity(
    model='cube',
    scale=(350, 2, 350),
    position=(0, -1, 0),
    color=color.rgb(48, 68, 38),
    texture='white_cube',
    texture_scale=(175, 175),
    collider='box'
)

# Дорога из деревни
stone_road = Entity(
    model='cube',
    scale=(8, 2.02, 120),
    position=(10, -1, 40),
    color=color.rgb(75, 65, 52),
    texture='white_cube',
    texture_scale=(4, 60)
)

# =====================================================================
# 3. ВИЗУАЛЬНЫЕ ЭФФЕКТЫ И ПАРТИКЛЫ
# =====================================================================
class FloatingText(Entity):
    def __init__(self, pos, text_str, text_col=color.white):
        super().__init__(
            position=pos + Vec3(random.uniform(-0.2, 0.2), 2.0, random.uniform(-0.2, 0.2))
        )
        self.label = Text(
            parent=self,
            text=text_str,
            color=text_col,
            scale=7.5,
            billboard=True,
            origin=(0, 0)
        )
        self.lifetime = 1.1

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

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

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

class YrdenRune(Entity):
    def __init__(self, pos):
        super().__init__(
            model='cylinder',
            scale=(9.0, 0.04, 9.0),
            color=color.rgba(180, 50, 240, 110),
            position=pos + Vec3(0, 0.05, 0)
        )
        self.life = 8.0

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

# =====================================================================
# 4. ПОСТРОЙКИ И ДЕКОРАЦИИ
# =====================================================================
class House(Entity):
    def __init__(self, pos, rot_y=0):
        super().__init__(position=pos, rotation_y=rot_y)
        self.walls = Entity(parent=self, model='cube', scale=(8.5, 4.8, 9.5), color=color.rgb(78, 52, 34), position=(0, 2.4, 0), collider='box')
        self.roof = Entity(parent=self, model='cone', scale=(10.5, 4.0, 11.5), color=color.rgb(130, 100, 55), position=(0, 6.4, 0), rotation=(0, 45, 0))
        self.door = Entity(parent=self, model='cube', scale=(1.8, 3.0, 0.2), color=color.rgb(38, 22, 14), position=(0, 1.5, -4.8))

class PineTree(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.trunk = Entity(parent=self, model='cylinder', scale=(0.7, 5.5, 0.7), color=color.rgb(55, 36, 24), position=(0, 2.75, 0), collider='box')
        self.c1 = Entity(parent=self, model='cone', scale=(5.0, 4.5, 5.0), color=color.rgb(25, 48, 25), position=(0, 6.0, 0))
        self.c2 = Entity(parent=self, model='cone', scale=(3.8, 4.0, 3.8), color=color.rgb(32, 58, 32), position=(0, 8.2, 0))

class AncientMonolith(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.body = Entity(parent=self, model='cube', scale=(2.2, 8.0, 2.2), color=color.rgb(72, 75, 78), position=(0, 4.0, 0), collider='box')
        self.top = Entity(parent=self, model='cone', scale=(3.0, 2.2, 3.0), color=color.rgb(52, 55, 58), position=(0, 8.9, 0))

# Постройка деревни и капища
houses_list = [
    House(pos=(14, 0, 16), rot_y=20),
    House(pos=(-18, 0, 22), rot_y=-35),
    House(pos=(20, 0, -20), rot_y=110)
]

monoliths_list = [
    AncientMonolith(pos=(85, 0, 85)),
    AncientMonolith(pos=(75, 0, 95)),
    AncientMonolith(pos=(95, 0, 75))
]

random.seed(42)
for _ in range(60):
    tx = random.uniform(-150, 150)
    tz = random.uniform(-150, 150)
    if abs(tx) > 22 or abs(tz) > 22:
        PineTree(pos=(tx, 0, tz))

# =====================================================================
# 5. ИНТЕРАКТИВНЫЕ ОБЪЕКТЫ (СУНДУКИ, ТРАВЫ)
# =====================================================================
class Chest(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(115, 75, 40), position=(0, 0.45, 0), collider='box')
        self.lock = Entity(parent=self.box, model='cube', scale=(0.14, 0.22, 0.14), color=color.gold, position=(0, 0, 0.52))
        self.looted = False

    def open_chest(self, player_ref):
        if not self.looted:
            self.looted = True
            c = random.randint(40, 85)
            player_ref.coins += c
            player_ref.potions += 1
            FloatingText(self.position, f"+{c} Крон | +1 Ласточка", color.gold)
            self.box.color = color.gray

class Herb(Entity):
    def __init__(self, pos):
        super().__init__(position=pos)
        self.model_part = Entity(parent=self, model='sphere', scale=(0.7, 0.4, 0.7), color=color.rgb(45, 185, 75), position=(0, 0.2, 0), collider='box')
        self.flower = Entity(parent=self, model='sphere', scale=0.35, color=color.rgb(240, 255, 95), position=(0, 0.42, 0))
        self.picked = False

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

chests = [Chest(pos=(22, 0, 22)), Chest(pos=(-22, 0, 26)), Chest(pos=(88, 0, 88))]
herbs = [Herb(pos=(8, 0, 12)), Herb(pos=(-10, 0, 6)), Herb(pos=(28, 0, -8)), Herb(pos=(65, 0, 55))]

# =====================================================================
# 6. ГЕРАЛЬТ (ИГРОК С ПОЛНОЙ АНИМАЦИЕЙ И МЕХАНИКАМИ)
# =====================================================================
class Witcher(Entity):
    def __init__(self):
        super().__init__(position=(0, 0, 0))
        
        self.mesh_root = Entity(parent=self, position=(0, 0, 0))

        # Доспех Школы Волка
        self.torso = Entity(parent=self.mesh_root, model='cube', scale=(0.9, 1.4, 0.5), color=color.rgb(42, 38, 34), position=(0, 1.35, 0))
        self.vest = Entity(parent=self.torso, model='cube', scale=(1.02, 0.65, 1.05), color=color.rgb(82, 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.mesh_root, 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_l = Entity(parent=self.mesh_root, model='cube', scale=(0.32, 0.85, 0.35), color=color.rgb(24, 20, 16), position=(-0.24, 0.42, 0))
        self.leg_r = Entity(parent=self.mesh_root, 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.sword_blade = Entity(parent=self.mesh_root, 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_mesh = 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 = 160
        self.potions = 3
        self.herbs = 2
        self.sword = "SILVER" # SILVER / STEEL
        self.is_attacking = False
        self.quen_active = False
        self.anim_t = 0.0

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

    def reset_blade(self):
        self.sword_blade.rotation = (75, 0, 0)
        self.sword_blade.enabled = False
        self.is_attacking = False

    def heal(self):
        if self.potions > 0 and self.toxicity <= 80:
            self.potions -= 1
            self.hp = min(self.max_hp, self.hp + 70)
            self.toxicity += 30
            FloatingText(self.position, "+70 HP (Ласточка)", color.green)
        elif self.toxicity > 80:
            FloatingText(self.position, "Интоксикация слишком высока!", color.red)

player = Witcher()

# =====================================================================
# 7. БЕЗОТКАЗНАЯ ОРБИТАЛЬНАЯ КАМЕРА (БЕЗ ОТСЕЧЕНИЙ)
# =====================================================================
class OrbitCam:
    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] * 125
        self.pitch = clamp(self.pitch - mouse.velocity[1] * 80, -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.5, 0))

cam = OrbitCam(player)
mouse.locked = True

# =====================================================================
# 8. ВРАГИ: УТОПЦЫ И БОСС ЛЕШИЙ
# =====================================================================
class Drowner(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_mesh.enabled = False
                FloatingText(player.position, "КВЕН СПАС ОТ УДАРА!", color.yellow)
            else:
                player.hp -= 24
                FloatingText(player.position, "-24 HP!", color.red)

class LeshenBoss(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)
            FloatingText(self.position, "*Растворился в тумане*", color.smoke)

        if self.root_cd <= 0 and dist < 32.0:
            self.root_cd = 5.0
            spawn_roots(player.position)
            FloatingText(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_roots(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 = [Drowner(pos=(32, 0, 30)), Drowner(pos=(38, 0, 36)), Drowner(pos=(-35, 0, 35))]
leshen = LeshenBoss(pos=(85, 0, 85))

# =====================================================================
# 9. СЮЖЕТНЫЕ NPC (СТАРОСТА, ТРАВНИЦА, БАРОН)
# =====================================================================
class NPC(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 = NPC(pos=(14, 0, 15), name="Староста Богдан", role="elder")
npc_tomira = NPC(pos=(-16, 0, 20), name="Травница Томира", role="herbalist")
npc_baron = NPC(pos=(18, 0, -16), name="Кровавый Барон", role="baron")

# =====================================================================
# 10. КВЕСТЫ И БЕСТИАРИЙ
# =====================================================================
quest_db = {
    "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_panel = WindowPanel(
    title='Диалог',
    content=(
        Text(text="", id='d_text'),
        Button(text='1. Согласиться / Выполнить', color=color.azure),
        Button(text='2. Отказаться', color=color.gray)
    ),
    enabled=False
)

menu_panel = 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_tab = "INV"

def update_menu():
    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":
        lines = []
        for qid, q in quest_db.items():
            st_str = "ВЫПОЛНЕН" if q["stage"] == 3 else ("АКТИВЕН" if q["stage"] > 0 else "НЕ НАЧАТ")
            lines.append(f"• {q['name']} — [{st_str}]\n  {q['desc']}")
        menu_panel.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_panel.enabled and not dialog_panel.enabled:
        cam.update()

        move_vec = Vec3(
            (held_keys['d'] - held_keys['a']),
            0,
            (held_keys['w'] - held_keys['s'])
        ).normalized()

        if move_vec.length() > 0:
            player.anim_t += 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.mesh_root.rotation_y = math.degrees(math.atan2(world_move.x, world_move.z))

            player.leg_l.rotation_x = math.sin(player.anim_t) * 28
            player.leg_r.rotation_x = -math.sin(player.anim_t) * 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(28, 24, 20)
        for d in drowners:
            if d.alive: d.body.color = color.red
        if leshen.alive: leshen.body.color = color.rgb(255, 75, 0)
    else:
        window.color = color.rgb(85, 120, 160)
        for d in drowners:
            if d.alive: d.body.color = color.rgb(40, 115, 125)
        if leshen.alive: leshen.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_mesh.enabled = False
                FloatingText(player.position, "КВЕН РАЗБИТ КОРНЯМИ!", color.yellow)
            else:
                player.hp -= 38 * time.dt
                FloatingText(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_tab

    if key == 'i':
        menu_panel.enabled = not menu_panel.enabled
        mouse.locked = not menu_panel.enabled
        update_menu()

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

    if dialog_panel.enabled:
        if key in ('1', 'e'): dialog_panel.content[1].on_click()
        elif key in ('2', 'escape'):
            dialog_panel.enabled = False
            mouse.locked = True
        return

    # Атака (ЛКМ)
    if key == 'left mouse down':
        player.attack()
        
        for d in drowners:
            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
                FloatingText(d.position, f"-{dmg} HP" if mult == 1.0 else f"-{dmg} HP (Нужно Серебро!)", col)
                if d.hp <= 0:
                    d.alive = False
                    d.body.rotation_x = 90
                    d.y = 0.2
                    player.coins += 20
                    FloatingText(d.position, "УТОПЕЦ УБИТ! +20 Крон", color.gold)

        if leshen.alive and distance(player.position, leshen.position) < 4.2:
            mult = 1.0 if player.sword == "SILVER" else 0.2
            dmg = int(48 * mult)
            leshen.hp -= dmg
            FloatingText(leshen.position, f"-{dmg} HP", color.gold)
            if leshen.hp <= 0:
                leshen.alive = False
                leshen.body.rotation_x = 90
                leshen.y = 0.3
                quest_db["leshen"]["stage"] = 2
                FloatingText(leshen.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)
        FloatingText(player.position, f"Обнажен {player.sword} меч", col)

    # Зелье (F)
    if key == 'f':
        player.heal()

    # ЗНАКИ
    if key == '1' and player.stamina >= 35: # Игни
        player.stamina -= 35
        FloatingText(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:
            if d.alive and distance(player.position, d.position) < 9.0:
                d.hp -= 45
                FloatingText(d.position, "ОЖОГ ИГНИ: -45 HP!", color.orange)
        if leshen.alive and distance(player.position, leshen.position) < 11.0:
            leshen.hp -= 70
            FloatingText(leshen.position, "ЛЕШИЙ ГОРИТ: -70 HP!", color.orange)

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

    if key == '3' and player.stamina >= 35: # Аард
        player.stamina -= 35
        FloatingText(player.position, "ЗНАК ААРД!", color.cyan)
        for d in drowners:
            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
                FloatingText(d.position, "ААРД: СТАН!", color.cyan)

    if key == '4' and player.stamina >= 35: # Ирден
        player.stamina -= 35
        YrdenRune(player.position)
        FloatingText(player.position, "ЗНАК ИРДЕН!", color.magenta)
        for d in drowners:
            if distance(player.position, d.position) < 9.0: d.slow_timer = 6.0
        if distance(player.position, leshen.position) < 9.0: leshen.slow_timer = 6.0

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

    # Взаимодействие (E)
    if key == 'e':
        for ch in chests:
            if not ch.looted and distance(player.position, ch.position) < 2.5: ch.open_chest(player)
        for hb in herbs:
            if not hb.picked and distance(player.position, hb.position) < 2.5: hb.pick(player)

        if distance(player.position, npc_elder.position) < 3.8: open_dialog(npc_elder)
        elif distance(player.position, npc_tomira.position) < 3.8: open_dialog(npc_tomira)
        elif distance(player.position, npc_baron.position) < 3.8: open_dialog(npc_baron)

def open_dialog(npc):
    mouse.locked = False
    dialog_panel.enabled = True

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

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

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

# =====================================================================
# 14. ЗАПУСК
# =====================================================================
app.run()