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


from ursina import *
import math
import random

app = Ursina()

# Настройка окна, скрываем стандартный курсор и делаем камеру от 3-го лица
window.title = "Savannah Rider: Wild Hunt 3D"
window.borderless = False
window.fullscreen = False
window.color = color.rgb(40, 25, 10)  # Глубокий закатный фон саванны
window.fps_counter.enabled = True
mouse.locked = True

# --- ОСВЕЩЕНИЕ И АТМОСФЕРА ---
# Теплый закатный свет саванны с мягкими тенями
sun = DirectionalLight(y=3, z=2, shadows=True)
sun.color = color.rgb(255, 220, 150)
ambient = AmbientLight(color=color.rgb(70, 50, 30))

# Небо / атмосфера (большая сфера вокруг мира)
sky = Entity(model='sphere', scale=500, color=color.rgb(210, 120, 50), unlit=True)

# --- ЗЕМЛЯ САВАННЫ ---
ground = Entity(
    model='plane',
    scale=(300, 1, 300),
    color=color.rgb(130, 105, 50),
    texture='white_cube',
    texture_scale=(80, 80),
    collider='box'
)

# Декорации: Акации и камни
for _ in range(50):
    x = random.uniform(-140, 140)
    z = random.uniform(-140, 140)
    # Ствол акации
    trunk = Entity(model='cylinder', color=color.rgb(75, 50, 25), scale=(0.8, 7, 0.8), position=(x, 3.5, z), collider='box')
    # Раскидистая крона
    for i in range(3):
        Entity(model='sphere', color=color.rgb(45, 90, 30), scale=(random.uniform(4, 7), 2, random.uniform(4, 7)), position=(x + random.uniform(-1.5, 1.5), 6.5 + i*0.8, z + random.uniform(-1.5, 1.5)))

for _ in range(40):
    Entity(
        model='cube',
        color=color.rgb(110, 100, 90),
        scale=(random.uniform(2, 5), random.uniform(1, 2.5), random.uniform(2, 5)),
        position=(random.uniform(-140, 140), 0.5, random.uniform(-140, 140)),
        collider='box'
    )

# --- ЕЗДОВОЙ ЗВЕРЬ (НОСОРОГ) И ИГРОК ---
mount = Entity(position=(0, 0, 0))

# Тело носорога
body = Entity(model='cube', color=color.rgb(115, 110, 100), scale=(2.2, 2.0, 4.5), position=(0, 1.2, 0), parent=mount, collider='box')
# Голова
head = Entity(model='cube', color=color.rgb(100, 95, 85), scale=(1.3, 1.3, 1.8), position=(0, 1.7, 2.2), parent=mount)
# Рог
horn = Entity(model='cone', color=color.rgb(220, 220, 210), scale=(0.4, 1.8, 0.4), position=(0, 2.3, 2.8), rotation=(20, 0, 0), parent=mount)
# Ноги
for px, pz in [(-0.9, 1.5), (0.9, 1.5), (-0.9, -1.5), (0.9, -1.5)]:
    Entity(model='cylinder', color=color.rgb(90, 85, 75), scale=(0.6, 1.5, 0.6), position=(px, 0.75, pz), parent=mount)

# Фигура всадника (игрока) на спине
rider = Entity(model='cube', color=color.rgb(180, 120, 70), scale=(0.8, 1.4, 0.8), position=(0, 2.5, -0.2), parent=mount)
rider_head = Entity(model='sphere', color=color.rgb(210, 160, 120), scale=0.6, position=(0, 1.0, 0), parent=rider)

# Камера от третьего лица с плавным следованием
camera.parent = mount
camera.position = (0, 5, -9)
camera.rotation = (20, 0, 0)

# --- ИНВЕНТАРЬ И ОРУЖИЕ ---
weapons = ['Копье', 'Лук', 'Топор', 'Арбалет', 'Огненный Шар']
current_weapon_idx = 0

# UI элементов на экране
weapon_display = Text(text=f'Оружие: {weapons[current_weapon_idx]}', position=(-0.85, 0.44), scale=1.4, color=color.rgb(255, 230, 150))
hp_display = Text(text='Здоровье зверя: 100%', position=(-0.85, 0.38), scale=1.2, color=color.green)
controls_text = Text(
    text="Управление:\nWASD - Движение\nМышь - Обзор / Поворот\nКолесико мыши - Смена оружия\nЛКМ - Атака / Выстрел\nESC - Освободить курсор",
    position=(-0.85, 0.30), scale=1.0, color=color.rgb(200, 200, 200)
)

# Модель оружия в руке всадника
weapon_model = Entity(model='cube', color=color.brown, scale=(0.15, 0.15, 2.2), position=(0.8, 0.8, 0.5), parent=mount)

def update_weapon_visual():
    w = weapons[current_weapon_idx]
    weapon_display.text = f'Оружие: {w}'
    if w == 'Копье':
        weapon_model.model = 'cube'
        weapon_model.scale = (0.15, 0.15, 2.6)
        weapon_model.color = color.rgb(139, 90, 43)
    elif w == 'Лук':
        weapon_model.model = 'cube'
        weapon_model.scale = (0.2, 1.4, 0.2)
        weapon_model.color = color.rgb(100, 60, 20)
    elif w == 'Топор':
        weapon_model.model = 'cube'
        weapon_model.scale = (0.4, 0.9, 0.25)
        weapon_model.color = color.rgb(180, 180, 180)
    elif w == 'Арбалет':
        weapon_model.model = 'cube'
        weapon_model.scale = (0.7, 0.3, 1.3)
        weapon_model.color = color.rgb(60, 60, 60)
    elif w == 'Огненный Шар':
        weapon_model.model = 'sphere'
        weapon_model.scale = (0.6, 0.6, 0.6)
        weapon_model.color = color.rgb(255, 120, 0)

# --- СНАРЯДЫ И МАГИЯ ---
projectiles = []

class Projectile(Entity):
    def __init__(self, position, rotation, p_type):
        super().__init__(
            model='sphere' if p_type == 'Огненный Шар' else 'cube',
            color=color.rgb(255, 100, 0) if p_type == 'Огненный Шар' else color.rgb(70, 40, 10),
            scale=0.7 if p_type == 'Огненный Шар' else (0.2, 0.2, 1.2),
            position=position,
            rotation=rotation
        }
        self.p_type = p_type
        self.speed = 35 if p_type == 'Огненный Шар' else 45
        projectiles.append(self)

    def update(self):
        self.position += self.forward * self.speed * time.dt
        # Создание искр огня позади огненного шара
        if self.p_type == 'Огненный Шар' and random.random() < 0.5:
            Particle(position=self.position, color=color.rgb(255, 200, 50), scale=0.3)

        # Дистанция полета
        if distance(self.position, mount.position) > 120:
            projectiles.remove(self)
            destroy(self)
            return

        # Попадания по врагам
        for enemy in enemies:
            if distance(self, enemy) < 2.5:
                enemy.hp -= 60 if self.p_type == 'Огненный Шар' else 35
                # Эффект урона (вспышка)
                enemy.blink(color.red, duration=0.1)
                projectiles.remove(self)
                destroy(self)
                if enemy.hp <= 0:
                    enemies.remove(enemy)
                    destroy(enemy)
                break

class Particle(Entity):
    def __init__(self, position, color, scale):
        super().__init__(model='sphere', color=color, scale=scale, position=position)
        self.animate_scale(0, duration=0.3, curve=curve.out_expo)
        invoke(destroy, self, delay=0.3)

# --- ВРАГИ (ХИЩНИКИ САВАННЫ) ---
enemies = []

class Predator(Entity):
    def __init__(self, position):
        super().__init__(
            model='cube',
            color=color.rgb(190, 110, 40), # Львы / пантеры саванны
            scale=(1.6, 1.2, 2.8),
            position=position,
            collider='box'
        )
        self.hp = 70
        self.speed = random.uniform(3.5, 5.5)
        enemies.append(self)

    def update(self):
        # Преследование игрока
        if distance(self, mount) > 2.2:
            self.look_at(mount)
            self.position += self.forward * self.speed * time.dt
            # Анимация бега (покачивание)
            self.y = 0.6 + math.sin(time.time() * 20 + self.x) * 0.15
        else:
            # Нанесение урона игроку
            global player_hp
            pass

# Спавн хищников
for _ in range(12):
    Predator(position=(random.uniform(-120, 120), 0.6, random.uniform(-120, 120)))

# --- УПРАВЛЕНИЕ И ВЗАИМОДЕЙСТВИЕ ---
def input(key):
    global current_weapon_idx
    if key == 'scroll up':
        current_weapon_idx = (current_weapon_idx + 1) % len(weapons)
        update_weapon_visual()
    elif key == 'scroll down':
        current_weapon_idx = (current_weapon_idx - 1) % len(weapons)
        update_weapon_visual()
    
    elif key == 'left mouse down':
        w = weapons[current_weapon_idx]
        # Анимация замаха оружия (поворот модели)
        weapon_model.animate_rotation_y(weapon_model.rotation_y + 45, duration=0.1)
        weapon_model.animate_rotation_y(weapon_model.rotation_y, duration=0.1, delay=0.1)

        if w in ['Лук', 'Арбалет', 'Огненный Шар']:
            Projectile(position=weapon_model.world_position, rotation=mount.world_rotation, p_type=w)
        else:
            # Атака ближнего боя (Копье / Топор)
            for enemy in enemies:
                if distance(mount, enemy) < 4.5:
                    enemy.hp -= 45
                    enemy.blink(color.red, duration=0.1)
                    if enemy.hp <= 0:
                        enemies.remove(enemy)
                        destroy(enemy)
                    break

    elif key == 'escape':
        # Переключение видимости курсора
        mouse.locked = not mouse.locked

# --- ИГРОВОЙ ЦИКЛ ---
def update():
    move_speed = 12
    
    # Вращение ездового зверя вслед за взглядом мыши (поворот камеры)
    mount.rotation_y += mouse.velocity[0] * 40

    # Движение вперед/назад/влево/вправо относительно направления взгляда
    move_dir = Vec3(0, 0, 0)
    is_moving = False

    if held_keys['w']:
        move_dir += mount.forward
        is_moving = True
    if held_keys['s']:
        move_dir -= mount.forward
        is_moving = True
    if held_keys['a']:
        move_dir -= mount.right
        is_moving = True
    if held_keys['d']:
        move_dir += mount.right
        is_moving = True

    if is_moving:
        move_dir = move_dir.normalized()
        mount.position += move_dir * move_speed * time.dt
        # Плавная процедурная анимация бега/шага зверя (покачивание вверх-вниз)
        mount.y = math.sin(time.time() * 18) * 0.2
        # Небольшой наклон тела при поворотах
        body.rotation_z = math.sin(time.time() * 9) * 3
    else:
        mount.y = 0
        body.rotation_z = 0

    # Ограничение перемещения по границам карты
    mount.x = clamp(mount.x, -145, 145)
    mount.z = clamp(mount.z, -145, 145)

app.run()