Загрузка данных
from ursina import *
import math
import random
app = Ursina()
# Настройка окна и фона (делаем темно-песочным, чтобы не было ослепляющего белого)
window.title = "Savannah Rider: Wild Hunt 3D"
window.borderless = False
window.fullscreen = False
window.color = color.rgb(80, 50, 20)
window.fps_counter.enabled = True
mouse.locked = True
# --- ОСВЕЩЕНИЕ (Исправленное, без пересвета) ---
# Используем мягкий рассеянный свет вместо слепящих направленных источников
ambient_light = AmbientLight(color=color.rgb(200, 180, 140))
sun = DirectionalLight()
sun.color = color.rgb(255, 240, 200)
sun.rotation = (45, -45, 0)
# --- ЗЕМЛЯ САВАННЫ ---
ground = Entity(
model='plane',
scale=(400, 1, 400),
color=color.rgb(150, 120, 60),
texture='white_cube',
texture_scale=(100, 100),
collider='box'
)
# Декорации саванны: деревья и камни
for _ in range(60):
x = random.uniform(-180, 180)
z = random.uniform(-180, 180)
# Ствол дерева
Entity(model='cylinder', color=color.rgb(90, 60, 30), scale=(1, 8, 1), position=(x, 4, z), collider='box')
# Крона дерева
Entity(model='sphere', color=color.rgb(50, 110, 30), scale=(6, 3, 6), position=(x, 8, z))
for _ in range(40):
Entity(
model='cube',
color=color.rgb(120, 110, 100),
scale=(random.uniform(2, 6), random.uniform(1, 3), random.uniform(2, 6)),
position=(random.uniform(-180, 180), 0.5, random.uniform(-180, 180)),
collider='box'
)
# --- ЕЗДОВОЙ ЗВЕРЬ (НОСОРОГ) И ИГРОК ---
mount = Entity(position=(0, 0, 0))
# Тело носорога
body = Entity(model='cube', color=color.rgb(110, 105, 95), scale=(2.2, 2.0, 4.5), position=(0, 1.2, 0), parent=mount, collider='box')
head = Entity(model='cube', color=color.rgb(95, 90, 80), scale=(1.3, 1.3, 1.8), position=(0, 1.7, 2.2), parent=mount)
horn = Entity(model='cone', color=color.rgb(230, 230, 220), scale=(0.4, 2.0, 0.4), position=(0, 2.4, 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(80, 75, 65), scale=(0.6, 1.5, 0.6), position=(px, 0.75, pz), parent=mount)
# Всадник (игрок) на спине
rider = Entity(model='cube', color=color.rgb(190, 130, 80), scale=(0.8, 1.4, 0.8), position=(0, 2.5, -0.2), parent=mount)
rider_head = Entity(model='sphere', color=color.rgb(220, 170, 130), scale=0.6, position=(0, 1.0, 0), parent=rider)
# Камера от третьего лица (стабильная подвеска позади зверя)
camera.parent = mount
camera.position = (0, 5, -10)
camera.rotation = (20, 0, 0)
# --- ИНВЕНТАРЬ И ОРУЖИЕ ---
weapons = ['Копье', 'Лук', 'Топор', 'Арбалет', 'Огненный Шар']
current_weapon_idx = 0
# Текстовый интерфейс на экране
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)
Text(
text="Управление:\nWASD - Движение\nМышь - Обзор\nКолесико мыши - Смена оружия\nЛКМ - Атака / Выстрел\nESC - Курсор",
position=(-0.85, 0.30), scale=1.0, color=color.rgb(220, 220, 220)
)
# Модель оружия в руке
weapon_model = Entity(model='cube', color=color.rgb(139, 90, 43), scale=(0.15, 0.15, 2.6), 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.8)
weapon_model.color = color.rgb(139, 90, 43)
elif w == 'Лук':
weapon_model.model = 'cube'
weapon_model.scale = (0.2, 1.5, 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, 120, 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 = 40 if p_type == 'Огненный Шар' else 50
projectiles.append(self)
def update(self):
self.position += self.forward * self.speed * time.dt
# Эффект искр для огненного шара
if self.p_type == 'Огненный Шар' and random.random() < 0.6:
Particle(position=self.position, color=color.rgb(255, 200, 50), scale=0.4)
if distance(self.position, mount.position) > 150:
projectiles.remove(self)
destroy(self)
return
# Попадания по врагам
for enemy in enemies:
if distance(self, enemy) < 2.5:
enemy.hp -= 70 if self.p_type == 'Огненный Шар' else 40
enemy.blink(color.red, duration=0.15)
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(200, 120, 40),
scale=(1.6, 1.2, 2.8),
position=position,
collider='box'
)
self.hp = 80
self.speed = random.uniform(4, 6)
enemies.append(self)
def update(self):
if distance(self, mount) > 2.5:
self.look_at(mount)
self.position += self.forward * self.speed * time.dt
# Анимация бега зверя
self.y = 0.6 + math.sin(time.time() * 22 + self.x) * 0.2
for _ in range(15):
Predator(position=(random.uniform(-150, 150), 0.6, random.uniform(-150, 150)))
# --- УПРАВЛЕНИЕ ---
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 + 50, duration=0.08)
weapon_model.animate_rotation_y(weapon_model.rotation_y, duration=0.08, delay=0.08)
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) < 5.0:
enemy.hp -= 50
enemy.blink(color.red, duration=0.15)
if enemy.hp <= 0:
enemies.remove(enemy)
destroy(enemy)
break
elif key == 'escape':
mouse.locked = not mouse.locked
# --- ИГРОВОЙ ЦИКЛ ---
def update():
move_speed = 14
mount.rotation_y += mouse.velocity[0] * 45
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() * 20) * 0.25
body.rotation_z = math.sin(time.time() * 10) * 4
else:
mount.y = 0
body.rotation_z = 0
# Границы мира
mount.x = clamp(mount.x, -180, 180)
mount.z = clamp(mount.z, -180, 180)
app.run()