Загрузка данных
from ursina import *
import math
app = Ursina(title="The Witcher 3D", borderless=False)
window.color = color.rgb(135, 175, 210) # Чистое небо вместо серого тумана
# Освещение
DirectionalLight(y=15, z=-10, shadows=True, rotation=(45, -45, 0))
AmbientLight(color=color.rgba(160, 160, 170, 200))
# Земля и трава
ground = Entity(
model='plane',
scale=(120, 1, 120),
color=color.rgb(65, 95, 55),
texture='white_cube',
texture_scale=(60, 60),
collider='box'
)
# Деревня: Изба и забор
house = Entity(model='cube', color=color.rgb(90, 60, 40), scale=(8, 5, 8), position=(10, 2.5, 12), collider='box')
roof = Entity(model='cone', color=color.rgb(120, 90, 50), scale=(10, 3.5, 10), position=(10, 6, 12))
# 3D Ведьмак
player = Entity(position=(0, 1, 0))
# Корпус
p_body = Entity(parent=player, model='cube', scale=(0.8, 1.4, 0.4), color=color.rgb(40, 35, 30))
# Голова и волосы
p_head = Entity(parent=player, model='sphere', scale=0.5, color=color.rgb(220, 190, 160), position=(0, 0.95, 0))
p_hair = Entity(parent=player, model='sphere', scale=(0.54, 0.3, 0.54), color=color.rgb(240, 240, 245), position=(0, 1.1, -0.05))
# Мечи за спиной
Entity(parent=p_body, model='cube', scale=(0.06, 1.6, 0.06), color=color.gray, position=(-0.2, 0.1, -0.25), rotation=(0, 0, -25))
Entity(parent=p_body, model='cube', scale=(0.06, 1.6, 0.06), color=color.cyan, position=(0.2, 0.1, -0.25), rotation=(0, 0, 25))
# Ручной меч для удара
sword_mesh = Entity(parent=player, model='cube', scale=(0.06, 1.4, 0.06), color=color.white, position=(0.6, 0.2, 0.5), rotation=(80, 0, 0), enabled=False)
# Щит Квен
quen_shield = Entity(parent=player, model='sphere', scale=2.5, color=color.rgba(255, 215, 0, 80), enabled=False)
# Характеристики
player_stats = {
"hp": 200,
"max_hp": 200,
"stamina": 100,
"speed": 7,
"attacking": False,
"quen": False
}
# Враг: Утопец
drowner = Entity(model='cube', scale=(1, 1.6, 0.8), color=color.rgb(40, 120, 130), position=(15, 1, 20), collider='box')
Entity(parent=drowner, model='sphere', scale=0.15, color=color.red, position=(-0.25, 0.4, 0.45))
Entity(parent=drowner, model='sphere', scale=0.15, color=color.red, position=(0.25, 0.4, 0.45))
drowner_hp = 80
# NPC: Староста
elder = Entity(model='cube', scale=(0.8, 1.4, 0.4), color=color.rgb(160, 140, 110), position=(8, 1, 8), collider='box')
Entity(parent=elder, model='sphere', scale=0.5, color=color.rgb(210, 180, 150), position=(0, 0.9, 0))
quest_icon = Entity(parent=elder, model='sphere', scale=0.25, color=color.yellow, position=(0, 1.6, 0))
# Камера 3-го лица
cam_yaw = 0
cam_pitch = 20
cam_dist = 6.5
mouse.locked = True
# Интерфейс
hud = Text(text="HP: 200 | Энергия: 100 | [ЛКМ] Меч [1] Игни [2] Квен", position=(-0.85, 0.45), scale=1.2, color=color.white)
msg_text = Text(text="", origin=(0, 0), position=(0, 0.25), scale=1.8, color=color.yellow)
def show_msg(txt):
msg_text.text = txt
invoke(setattr, msg_text, 'text', "", delay=2.0)
def update():
global cam_yaw, cam_pitch, drowner_hp
# Энергия
if player_stats["stamina"] < 100:
player_stats["stamina"] = min(100, player_stats["stamina"] + 25 * time.dt)
# Вращение камеры мышью
cam_yaw += mouse.velocity[0] * 120
cam_pitch = clamp(cam_pitch - mouse.velocity[1] * 80, -5, 50)
rad_y = math.radians(cam_yaw)
rad_p = math.radians(cam_pitch)
cx = player.x - cam_dist * math.sin(rad_y) * math.cos(rad_p)
cz = player.z - cam_dist * math.cos(rad_y) * math.cos(rad_p)
cy = player.y + 2.2 + cam_dist * math.sin(rad_p)
camera.position = Vec3(cx, cy, cz)
camera.look_at(player.position + Vec3(0, 1.4, 0))
# Движение
move = Vec3(
(held_keys['d'] - held_keys['a']),
0,
(held_keys['w'] - held_keys['s'])
).normalized()
if move.length() > 0:
fwd = Vec3(math.sin(rad_y), 0, math.cos(rad_y))
rgt = Vec3(math.cos(rad_y), 0, -math.sin(rad_y))
wdir = fwd * move.z + rgt * move.x
player.position += wdir * player_stats["speed"] * time.dt
player.rotation_y = math.degrees(math.atan2(wdir.x, wdir.z))
# ИИ Утопца
if drowner_hp > 0:
dist = distance(player.position, drowner.position)
if 1.8 < dist < 22:
drowner.look_at_2d(player.position)
drowner.position += drowner.forward * 3.2 * time.dt
elif dist <= 1.8:
if player_stats["quen"]:
player_stats["quen"] = False
quen_shield.enabled = False
show_msg("КВЕН ПОГЛОТИЛ УДАР!")
else:
player_stats["hp"] -= 15 * time.dt
if player_stats["hp"] <= 0:
show_msg("ВЫ ПОГИБЛИ")
# Обновление HUD
hud.text = f"HP: {int(player_stats['hp'])} | Энергия: {int(player_stats['stamina'])}"
def input(key):
global drowner_hp
# ЛКМ — Атака
if key == 'left mouse down' and player_stats["stamina"] >= 15:
player_stats["stamina"] -= 15
sword_mesh.enabled = True
sword_mesh.animate_rotation((10, 60, -40), duration=0.12)
invoke(setattr, sword_mesh, 'enabled', False, delay=0.2)
if distance(player.position, drowner.position) < 3.2 and drowner_hp > 0:
drowner_hp -= 30
show_msg("УДАР МЕЧОМ: -30 HP!")
if drowner_hp <= 0:
drowner.rotation_x = 90
drowner.y = 0.4
show_msg("УТОПЕЦ ПОВЕРЖЕН!")
# 1 — Игни
if key == '1' and player_stats["stamina"] >= 35:
player_stats["stamina"] -= 35
fire = Entity(model='sphere', color=color.orange, scale=0.6, position=player.position + Vec3(0, 1, 0) + player.forward * 1.5)
fire.animate_position(fire.position + player.forward * 8, duration=0.4)
destroy(fire, delay=0.45)
if distance(player.position, drowner.position) < 7 and drowner_hp > 0:
drowner_hp -= 25
show_msg("ЗНАК ИГНИ: -25 HP!")
# 2 — Квен
if key == '2' and player_stats["stamina"] >= 35:
player_stats["stamina"] -= 35
player_stats["quen"] = True
quen_shield.enabled = True
show_msg("ЗНАК КВЕН АКТИВЕН")
# Взаимодействие с NPC
if key == 'e':
if distance(player.position, elder.position) < 3.5:
show_msg("Староста: 'Ведьмак, убей утопца на болоте!'")
if key == 'escape':
mouse.locked = not mouse.locked
app.run()