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


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

app = Ursina(title="The Witcher 3D: White Orchard Prologue", borderless=False)
window.size = (1200, 720)
window.fps_counter.enabled = True

# ----------------- СЮЖЕТ И ДАННЫЕ КВЕСТА -----------------
quest_state = {
    "stage": 0,  # 0: Пролог, 1: Поговорить со старостой, 2: Убить чудовище, 3: Сдать квест
    "cinematic": True,
    "cinematic_timer": 0.0
}

# ----------------- ОКРУЖЕНИЕ И СВЕТ -----------------
DirectionalLight(y=10, z=-10, shadows=True, rotation=(45, -45, 0))
AmbientLight(color=color.rgba(120, 120, 130, 200))
scene.fog_density = 0.02
scene.fog_color = color.rgb(40, 45, 50)

# Земля (Белый Сад / Велен)
ground = Entity(
    model='plane',
    scale=(160, 1, 160),
    color=color.rgb(45, 60, 40),
    texture='white_cube',
    texture_scale=(80, 80),
    collider='box'
)

# Декорации: Деревья, Камни, Костер
trees = []
for _ in range(35):
    tx = random.uniform(-60, 60)
    tz = random.uniform(-60, 60)
    if abs(tx) > 8 or abs(tz) > 8:
        # Ствол
        Entity(model='cylinder', color=color.rgb(60, 40, 25), scale=(0.8, 6, 0.8), position=(tx, 3, tz), collider='box')
        # Крона
        Entity(model='cone', color=color.rgb(30, 55, 30), scale=(5, 7, 5), position=(tx, 7, tz))

# Деревенская изба
house = Entity(model='cube', color=color.rgb(80, 55, 35), scale=(8, 5, 10), position=(14, 2.5, 12), collider='box')
roof = Entity(model='cone', color=color.rgb(110, 85, 45), scale=(11, 4, 13), position=(14, 6.5, 12), rotation=(0, 45, 0))

# Костер привала
campfire = Entity(model='sphere', color=color.orange, scale=(0.8, 0.4, 0.8), position=(0, 0.2, 0))

# ----------------- 3D МОДЕЛЬ ГЕРАЛЬТА -----------------
class WitcherPlayer(Entity):
    def __init__(self):
        super().__init__(position=(0, 1, 0))
        
        # Тело (Доспех Волка)
        self.body = Entity(parent=self, model='cube', scale=(0.9, 1.4, 0.5), color=color.rgb(50, 45, 40), position=(0, 0, 0))
        # Голова и седые волосы
        self.head = Entity(parent=self, model='sphere', scale=0.55, color=color.rgb(220, 190, 160), position=(0, 1.0, 0))
        self.hair = Entity(parent=self, model='sphere', scale=(0.58, 0.35, 0.6), color=color.rgb(240, 240, 245), position=(0, 1.15, -0.05))
        
        # Два меча за спиной (Стальной и Серебряный)
        self.steel_scabbard = Entity(parent=self.body, model='cube', scale=(0.08, 1.6, 0.08), color=color.gray, position=(-0.25, 0.2, -0.32), rotation=(0, 0, -25))
        self.silver_scabbard = Entity(parent=self.body, model='cube', scale=(0.08, 1.6, 0.08), color=color.cyan, position=(0.25, 0.2, -0.32), rotation=(0, 0, 25))
        
        # Клинок в руке (для ударов)
        self.sword_hand = Entity(parent=self, model='cube', scale=(0.08, 1.4, 0.08), color=color.white, position=(0.6, 0.2, 0.5), rotation=(80, 0, 0), enabled=False)

        # Характеристики
        self.hp = 200
        self.max_hp = 200
        self.stamina = 100
        self.speed = 7
        self.quen_active = False
        self.is_attacking = False
        self.attack_timer = 0.0

        # Визуальный щит Квен
        self.quen_sphere = Entity(parent=self, model='sphere', scale=2.8, color=color.rgba(255, 215, 0, 70), enabled=False)

    def attack(self):
        if not self.is_attacking and self.stamina >= 15:
            self.is_attacking = True
            self.attack_timer = 0.35
            self.stamina -= 15
            self.sword_hand.enabled = True
            self.sword_hand.animate_rotation((15, 60, -45), duration=0.15)
            invoke(self.reset_sword, delay=0.25)

    def reset_sword(self):
        self.sword_hand.rotation = (80, 0, 0)
        self.sword_hand.enabled = False
        self.is_attacking = False

    def cast_igni(self):
        if self.stamina >= 35:
            self.stamina -= 35
            # Сноп пламени
            for i in range(8):
                fire = Entity(
                    model='sphere',
                    color=color.orange,
                    scale=0.4,
                    position=self.position + Vec3(0, 0.8, 0) + self.forward * 1.2,
                    collider='sphere'
                )
                spread = Vec3(random.uniform(-0.3, 0.3), random.uniform(-0.1, 0.1), random.uniform(-0.2, 0.2))
                fire.animate_position(fire.position + (self.forward + spread) * 9, duration=0.45, curve=curve.linear)
                destroy(fire, delay=0.5)

    def cast_quen(self):
        if self.stamina >= 40:
            self.stamina -= 40
            self.quen_active = True
            self.quen_sphere.enabled = True

player = WitcherPlayer()

# ----------------- КАМЕРА 3-ГО ЛИЦА -----------------
class ThirdPersonCamera:
    def __init__(self, target):
        self.target = target
        self.distance = 7
        self.height = 2.8
        self.pitch = 18
        self.yaw = 0

    def update(self):
        if quest_state["cinematic"]:
            return  # Во время катсцены камера управляется сценарием

        # Вращение орбиты мыши
        self.yaw += mouse.velocity[0] * 120
        self.pitch = clamp(self.pitch - mouse.velocity[1] * 80, -10, 60)

        # Вычисление координат камеры в сферической системе
        rad_yaw = math.radians(self.yaw)
        rad_pitch = math.radians(self.pitch)

        cx = self.target.x - self.distance * math.sin(rad_yaw) * math.cos(rad_pitch)
        cz = self.target.z - self.distance * math.cos(rad_yaw) * math.cos(rad_pitch)
        cy = self.target.y + self.height + self.distance * math.sin(rad_pitch)

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

cam_controller = ThirdPersonCamera(player)

# ----------------- 3D МОНСТР: УТОПЕЦ -----------------
class Drowner3D(Entity):
    def __init__(self, pos):
        super().__init__(
            model='cube',
            scale=(1.1, 1.6, 0.8),
            color=color.rgb(40, 110, 120),
            position=pos,
            collider='box'
        )
        # Глаза утопца
        Entity(parent=self, model='sphere', scale=0.15, color=color.red, position=(-0.25, 0.4, 0.45))
        Entity(parent=self, model='sphere', scale=0.15, color=color.red, position=(0.25, 0.4, 0.45))
        
        self.hp = 80
        self.speed = 3.6
        self.attack_cd = 0.0
        self.alive = True

    def update(self):
        if not self.alive or quest_state["cinematic"]:
            return

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

        # Преследование
        if 1.8 < dist < 30:
            self.look_at_2d(player.position)
            self.position += self.forward * self.speed * time.dt

        # Атака
        if dist <= 2.2 and self.attack_cd == 0:
            self.attack_cd = 1.4
            if player.quen_active:
                player.quen_active = False
                player.quen_sphere.enabled = False
                print_hud_message("КВЕН ПОГЛОТИЛ УДАР!")
            else:
                player.hp -= 25
                print_hud_message("УРОН: -25 HP!")

drowner = Drowner3D(pos=(28, 1, 26))

# ----------------- NPC: СТАРОСТА -----------------
npc_elder = Entity(model='cube', scale=(0.9, 1.4, 0.5), color=color.rgb(160, 140, 110), position=(11, 1, 8), collider='box')
Entity(parent=npc_elder, model='sphere', scale=0.5, color=color.rgb(210, 180, 150), position=(0, 0.9, 0))
# Иконка квеста
quest_marker = Entity(parent=npc_elder, model='sphere', scale=0.3, color=color.yellow, position=(0, 1.8, 0))

# ----------------- ИНТЕРФЕЙС / HUD И КАТСЦЕНЫ -----------------
fade_overlay = Entity(parent=camera.ui, model='quad', scale=(2, 2), color=color.black, z=-1)

cinematic_title = Text(
    text="ПРОЛОГ: БЕЛЫЙ САД\nВедьмаки никогда не спят спокойно...",
    origin=(0, 0),
    scale=1.8,
    color=color.gold,
    enabled=True
)

dialog_box = WindowPanel(
    title='Староста деревни',
    content=(
        Text("Ведьмак! К востоку от мельницы объявился Утопец-людоед.\nЗащити деревню, и 100 крон твои!"),
        Button(text='1. Беру заказ на чудовище.', color=color.azure),
        Button(text='2. Отказаться.', color=color.gray)
    ),
    enabled=False
)

hud_text = Text(text="", position=(-0.85, 0.45), scale=1.2, color=color.white)
status_msg = Text(text="", origin=(0, 0), position=(0, 0.25), scale=1.5, color=color.red, enabled=False)

def print_hud_message(msg):
    status_msg.text = msg
    status_msg.enabled = True
    invoke(setattr, status_msg, 'enabled', False, delay=1.5)

# ----------------- ГЛАВНЫЙ ИГРОВОЙ ЦИКЛ -----------------
def update():
    # 1. Режиссура катсцены
    if quest_state["cinematic"]:
        quest_state["cinematic_timer"] += time.dt
        t = quest_state["cinematic_timer"]

        # Плавное движение кинокамеры
        camera.position = Vec3(-6 + t * 0.8, 3 - t * 0.1, -8 + t * 0.6)
        camera.look_at(campfire.position + Vec3(0, 0.5, 0))

        # Выход из затемнения
        if t < 2.0:
            fade_overlay.color = color.rgba(0, 0, 0, int(255 * (1 - t / 2.0)))
        else:
            fade_overlay.color = color.rgba(0, 0, 0, 0)

        # Окончание пролога
        if t >= 5.0:
            quest_state["cinematic"] = False
            cinematic_title.enabled = False
            fade_overlay.enabled = False
            mouse.locked = True
            quest_state["stage"] = 1
        return

    # 2. Управление персонажем
    cam_controller.update()

    # Восстановление энергии
    if player.stamina < 100:
        player.stamina = min(100, player.stamina + 20 * time.dt)

    # Движение с привязкой к направлению камеры
    move_dir = Vec3(
        (held_keys['d'] - held_keys['a']),
        0,
        (held_keys['w'] - held_keys['s'])
    ).normalized()

    if move_dir.length() > 0:
        rad = math.radians(cam_controller.yaw)
        forward = Vec3(math.sin(rad), 0, math.cos(rad))
        right = Vec3(math.cos(rad), 0, -math.sin(rad))
        world_dir = forward * move_dir.z + right * move_dir.x

        player.position += world_dir * player.speed * time.dt
        # Поворот модельки Геральта лицом по направлению движения
        player.rotation_y = math.degrees(math.atan2(world_dir.x, world_dir.z))

    # Ведьмачье чутье (ПКМ): сгущение тумана и подсветка врага
    if mouse.right:
        scene.fog_density = 0.06
        scene.fog_color = color.rgb(15, 10, 10)
        drowner.color = color.rgb(255, 60, 0)
    else:
        scene.fog_density = 0.02
        scene.fog_color = color.rgb(40, 45, 50)
        drowner.color = color.rgb(40, 110, 120)

    # Интерактив с NPC (Клавиша E)
    dist_to_npc = distance(player.position, npc_elder.position)
    if dist_to_npc < 3.5:
        if quest_state["stage"] == 1:
            hud_text.text = "[E] Говорить со старостой"
            if held_keys['e'] and not dialog_box.enabled:
                dialog_box.enabled = True
                mouse.locked = False
        elif quest_state["stage"] == 3:
            hud_text.text = "[E] Забрать награду за чудовище"
            if held_keys['e']:
                print_hud_message("ЗАКАЗ ВЫПОЛНЕН: +100 КРОН!")
                quest_state["stage"] = 4
                quest_marker.enabled = False
    else:
        if dialog_box.enabled:
            dialog_box.enabled = False
            mouse.locked = True
        hud_text.text = f"HP: {int(player.hp)}/200 | Энергия: {int(player.stamina)} | Меч: Серебряный"


def input(key):
    if quest_state["cinematic"]:
        if key == 'space':  # Пропуск катсцены
            quest_state["cinematic_timer"] = 5.0
        return

    # Удар мечом на ЛКМ
    if key == 'left mouse down':
        player.attack()
        # Проверка попадания
        if distance(player.position, drowner.position) < 3.2 and drowner.alive:
            drowner.hp -= 35
            print_hud_message("УДАР СЕРЕБРОМ: -35 HP!")
            if drowner.hp <= 0:
                drowner.alive = False
                drowner.rotation_x = 90
                drowner.y = 0.4
                print_hud_message("УТОПЕЦ ПОВЕРЖЕН! Вернитесь к старосте.")
                quest_state["stage"] = 3

    # Знаки Ведьмака
    if key == '1':
        player.cast_igni()
        if distance(player.position, drowner.position) < 8.0 and drowner.alive:
            drowner.hp -= 25
            print_hud_message("ЗНАК ИГНИ: ОЖОГ -25 HP!")

    if key == '2':
        player.cast_quen()

    # Диалог
    if dialog_box.enabled:
        if key == '1':
            quest_state["stage"] = 2
            dialog_box.enabled = False
            mouse.locked = True
            print_hud_message("КВЕСТ ПОЛУЧЕН: Выследите Утопца на болоте!")

app.run()