Загрузка данных
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()