Загрузка данных
from ursina import *
from ursina.prefabs.health_bar import HealthBar
import math
import random
import time
# =====================================================================
# 1. ИНИЦИАЛИЗАЦИЯ ИГРОВОГО ДВИЖКА И ОКНА
# =====================================================================
app = Ursina(
title="The Witcher 3D: Chronicles of the Continent",
borderless=False,
fullscreen=False
)
window.size = (1280, 760)
window.color = color.rgb(115, 145, 175)
window.fps_counter.enabled = True
# =====================================================================
# 2. ОСВЕЩЕНИЕ И ОКРУЖАЮЩАЯ СРЕДА
# =====================================================================
sun_light = DirectionalLight(y=35, z=-25, shadows=False, rotation=(45, -40, 0))
ambient_light = AmbientLight(color=color.rgba(160, 160, 175, 255))
# Огромная карта Велена
world_terrain = Entity(
model='plane',
scale=(400, 1, 400),
color=color.rgb(50, 70, 40),
texture='white_cube',
texture_scale=(200, 200),
collider='box'
)
# =====================================================================
# 3. ВСПОМОГАТЕЛЬНЫЕ КЛАССЫ И ВИЗУАЛЬНЫЕ ЭФФЕКТЫ
# =====================================================================
class FloatingCombatText(Entity):
def __init__(self, pos, text_str, text_col=color.white):
super().__init__(
parent=scene,
position=pos + Vec3(random.uniform(-0.3, 0.3), 1.8, random.uniform(-0.3, 0.3))
)
self.label = Text(
parent=self,
text=text_str,
color=text_col,
scale=8,
billboard=True,
origin=(0, 0)
)
self.lifetime = 1.0
self.speed = 1.5
def update(self):
self.y += self.speed * time.dt
self.lifetime -= time.dt
if self.lifetime <= 0:
destroy(self)
class FireProjectile(Entity):
def __init__(self, start_pos, dir_vector):
super().__init__(
model='sphere',
scale=0.5,
color=color.orange,
position=start_pos,
collider='sphere'
)
self.direction = dir_vector
self.speed = 22.0
self.lifetime = 0.55
def update(self):
self.position += self.direction * self.speed * time.dt
self.scale += Vec3(1.2, 1.2, 1.2) * time.dt
self.lifetime -= time.dt
if self.lifetime <= 0:
destroy(self)
class YrdenTrapEntity(Entity):
def __init__(self, pos):
super().__init__(
model='circle',
scale=9.0,
color=color.rgba(180, 50, 240, 90),
position=pos + Vec3(0, 0.08, 0),
rotation=(90, 0, 0)
)
self.lifetime = 8.0
def update(self):
self.lifetime -= time.dt
if self.lifetime <= 0:
destroy(self)
# =====================================================================
# 4. ДЕКОРАЦИИ И АРХИТЕКТУРА МИРА
# =====================================================================
class VillageHouse(Entity):
def __init__(self, pos, rot_y=0):
super().__init__(position=pos, rotation_y=rot_y)
# Сруб
self.base = Entity(parent=self, model='cube', scale=(9, 5, 10), color=color.rgb(75, 48, 30), position=(0, 2.5, 0), collider='box')
# Соломенная крыша
self.roof = Entity(parent=self, model='cone', scale=(11, 4.2, 12), color=color.rgb(125, 95, 50), position=(0, 6.6, 0), rotation=(0, 45, 0))
# Дверь
self.door = Entity(parent=self, model='cube', scale=(2.0, 3.2, 0.2), color=color.rgb(40, 22, 14), position=(0, 1.6, -5.05))
class ForestTree(Entity):
def __init__(self, pos):
super().__init__(position=pos)
self.trunk = Entity(parent=self, model='cylinder', scale=(0.7, 6, 0.7), color=color.rgb(55, 36, 22), position=(0, 3, 0), collider='box')
self.crown1 = Entity(parent=self, model='cone', scale=(5.0, 5.0, 5.0), color=color.rgb(25, 48, 25), position=(0, 6.5, 0))
self.crown2 = Entity(parent=self, model='cone', scale=(3.8, 4.5, 3.8), color=color.rgb(32, 58, 32), position=(0, 8.8, 0))
class AncientMonolith(Entity):
def __init__(self, pos):
super().__init__(position=pos)
self.pillar = Entity(parent=self, model='cube', scale=(2.4, 8.0, 2.4), color=color.rgb(70, 75, 75), position=(0, 4.0, 0), collider='box')
self.cap = Entity(parent=self, model='cone', scale=(3.2, 2.0, 3.2), color=color.rgb(50, 55, 55), position=(0, 8.8, 0))
# Генерация деревни Залипье
village_houses = [
VillageHouse(pos=(18, 0, 20), rot_y=15),
VillageHouse(pos=(-20, 0, 25), rot_y=-30),
VillageHouse(pos=(22, 0, -22), rot_y=120)
]
# Генерация капища Лешего
monoliths = [
AncientMonolith(pos=(85, 0, 85)),
AncientMonolith(pos=(75, 0, 95)),
AncientMonolith(pos=(95, 0, 75))
]
# Рассадка лесного массива
random.seed(1337)
for _ in range(65):
tx = random.uniform(-160, 160)
tz = random.uniform(-160, 160)
if abs(tx) > 20 or abs(tz) > 20:
ForestTree(pos=(tx, 0, tz))
# =====================================================================
# 5. ИНТЕРАКТИВНЫЕ ОБЪЕКТЫ (СУНДУКИ, ТРАВЫ)
# =====================================================================
class LootChest3D(Entity):
def __init__(self, pos):
super().__init__(position=pos)
self.box = Entity(parent=self, model='cube', scale=(1.4, 0.9, 0.9), color=color.rgb(110, 70, 35), position=(0, 0.45, 0), collider='box')
self.lock = Entity(parent=self.box, model='cube', scale=(0.15, 0.2, 0.15), color=color.gold, position=(0, 0, 0.52))
self.looted = False
def loot(self, player_ref):
if not self.looted:
self.looted = True
gold_gain = random.randint(35, 75)
player_ref.coins += gold_gain
player_ref.potions += 1
FloatingCombatText(self.position, f"+{gold_gain} Крон | +1 Ласточка", color.gold)
self.box.color = color.gray
class HerbPatch3D(Entity):
def __init__(self, pos):
super().__init__(position=pos)
self.stem = Entity(parent=self, model='sphere', scale=(0.7, 0.4, 0.7), color=color.rgb(40, 180, 70), position=(0, 0.2, 0), collider='box')
self.flower = Entity(parent=self, model='sphere', scale=0.35, color=color.rgb(235, 245, 90), position=(0, 0.4, 0))
self.picked = False
def harvest(self, player_ref):
if not self.picked:
self.picked = True
player_ref.herbs += 1
FloatingCombatText(self.position, "+1 Ласточкина трава", color.green)
self.flower.enabled = False
chests_in_world = [
LootChest3D(pos=(24, 0, 24)),
LootChest3D(pos=(-25, 0, 30)),
LootChest3D(pos=(90, 0, 90))
]
herbs_in_world = [
HerbPatch3D(pos=(8, 0, 15)),
HerbPatch3D(pos=(-12, 0, 8)),
HerbPatch3D(pos=(30, 0, -10)),
HerbPatch3D(pos=(60, 0, 50)),
HerbPatch3D(pos=(75, 0, 65))
]
# =====================================================================
# 6. ГЕРАЛЬТ ИЗ РИВИИ (ИГРОК 3-ГО ЛИЦА)
# =====================================================================
class WitcherPlayer(Entity):
def __init__(self):
super().__init__(position=(0, 0, 0))
# Корневой контейнер визуала
self.visual = Entity(parent=self, position=(0, 0, 0))
# Тело (Доспех Школы Волка)
self.torso = Entity(parent=self.visual, model='cube', scale=(0.9, 1.4, 0.5), color=color.rgb(45, 40, 35), position=(0, 1.35, 0))
self.vest = Entity(parent=self.torso, model='cube', scale=(1.02, 0.65, 1.05), color=color.rgb(80, 48, 30), position=(0, 0.1, 0))
self.medallion = Entity(parent=self.torso, model='sphere', scale=0.15, color=color.white, position=(0, 0.35, 0.55))
# Голова и седые волосы
self.head = Entity(parent=self.visual, model='sphere', scale=0.52, color=color.rgb(225, 190, 160), position=(0, 2.3, 0))
self.hair = Entity(parent=self.head, model='sphere', scale=(1.08, 0.55, 1.1), color=color.rgb(245, 245, 250), position=(0, 0.25, -0.05))
# Ноги для анимации бега
self.leg_left = Entity(parent=self.visual, model='cube', scale=(0.32, 0.85, 0.35), color=color.rgb(24, 20, 16), position=(-0.24, 0.42, 0))
self.leg_right = Entity(parent=self.visual, model='cube', scale=(0.32, 0.85, 0.35), color=color.rgb(24, 20, 16), position=(0.24, 0.42, 0))
# Два меча за спиной
self.sheath_steel = Entity(parent=self.torso, model='cube', scale=(0.06, 1.6, 0.06), color=color.gray, position=(-0.24, 0.2, -0.28), rotation=(0, 0, -25))
self.sheath_silver = Entity(parent=self.torso, model='cube', scale=(0.06, 1.6, 0.06), color=color.cyan, position=(0.24, 0.2, -0.28), rotation=(0, 0, 25))
# Боевой клинок в руке
self.hand_blade = Entity(parent=self.visual, model='cube', scale=(0.06, 1.6, 0.06), color=color.cyan, position=(0.68, 1.25, 0.45), rotation=(75, 0, 0), enabled=False)
# Сфера защитного Знака Квен
self.quen_sphere = Entity(parent=self, model='sphere', scale=3.3, color=color.rgba(255, 215, 0, 80), position=(0, 1.4, 0), enabled=False)
# Характеристики
self.hp = 250
self.max_hp = 250
self.stamina = 100
self.max_stamina = 100
self.toxicity = 0
self.speed = 8.5
# Инвентарь и боевой режим
self.coins = 180
self.potions = 3
self.herbs = 2
self.sword = "SILVER" # "SILVER" или "STEEL"
self.is_attacking = False
self.quen_active = False
self.dodge_active = False
self.anim_step = 0.0
def attack(self):
if not self.is_attacking and self.stamina >= 14:
self.is_attacking = True
self.stamina -= 14
self.hand_blade.color = color.cyan if self.sword == "SILVER" else color.rgb(230, 210, 170)
self.hand_blade.enabled = True
self.hand_blade.animate_rotation((15, 65, -45), duration=0.12)
invoke(self.reset_attack, delay=0.22)
def reset_attack(self):
self.hand_blade.rotation = (75, 0, 0)
self.hand_blade.enabled = False
self.is_attacking = False
def drink_potion(self):
if self.potions > 0 and self.toxicity <= 80:
self.potions -= 1
self.hp = min(self.max_hp, self.hp + 70)
self.toxicity += 30
FloatingCombatText(self.position, "+70 HP (Ласточка)", color.green)
elif self.toxicity > 80:
FloatingCombatText(self.position, "Интоксикация критическая!", color.red)
player = WitcherPlayer()
# =====================================================================
# 7. КАМЕРА ТРЕТЬЕГО ЛИЦА С АБСОЛЮТНОЙ СТАБИЛИЗАЦИЕЙ
# =====================================================================
class WitcherCamera:
def __init__(self, target_entity):
self.target = target_entity
self.dist = 6.8
self.height = 2.4
self.yaw = 0.0
self.pitch = 18.0
def update(self):
self.yaw += mouse.velocity[0] * 125
self.pitch = clamp(self.pitch - mouse.velocity[1] * 80, -8, 52)
rad_y = math.radians(self.yaw)
rad_p = math.radians(self.pitch)
cx = self.target.x - self.dist * math.sin(rad_y) * math.cos(rad_p)
cz = self.target.z - self.dist * math.cos(rad_y) * math.cos(rad_p)
cy = self.target.y + self.height + self.dist * math.sin(rad_p)
camera.position = Vec3(cx, cy, cz)
camera.look_at(self.target.position + Vec3(0, 1.5, 0))
cam = WitcherCamera(player)
mouse.locked = True
# =====================================================================
# 8. ВРАГИ: УТОПЦЫ И ДРЕВНИЙ ЛЕШИЙ (БОСС)
# =====================================================================
class DrownerEntity(Entity):
def __init__(self, pos):
super().__init__(position=pos)
self.body = Entity(parent=self, model='cube', scale=(0.95, 1.5, 0.75), color=color.rgb(40, 115, 125), position=(0, 0.75, 0), collider='box')
Entity(parent=self.body, model='sphere', scale=0.14, color=color.red, position=(-0.25, 0.38, 0.4))
Entity(parent=self.body, model='sphere', scale=0.14, color=color.red, position=(0.25, 0.38, 0.4))
self.hp = 90
self.max_hp = 90
self.speed = 3.6
self.attack_cd = 0.0
self.stun_timer = 0.0
self.slow_timer = 0.0
self.alive = True
def update(self):
if not self.alive:
return
if self.stun_timer > 0:
self.stun_timer -= time.dt
return
if self.slow_timer > 0:
self.slow_timer -= time.dt
spd = self.speed * 0.35 if self.slow_timer > 0 else self.speed
self.attack_cd = max(0.0, self.attack_cd - time.dt)
dist = distance(self.position, player.position)
if 2.0 < dist < 26.0:
self.look_at_2d(player.position)
self.position += self.forward * spd * time.dt
if dist <= 2.2 and self.attack_cd <= 0.0:
self.attack_cd = 1.3
if player.quen_active:
player.quen_active = False
player.quen_sphere.enabled = False
FloatingCombatText(player.position, "КВЕН ПОГЛОТИЛ УДАР!", color.yellow)
else:
player.hp -= 24
FloatingCombatText(player.position, "-24 HP!", color.red)
class AncientLeshenBoss(Entity):
def __init__(self, pos):
super().__init__(position=pos)
self.body = Entity(parent=self, model='cube', scale=(2.0, 3.6, 1.4), color=color.rgb(42, 28, 20), position=(0, 1.8, 0), collider='box')
self.skull = Entity(parent=self, model='cone', scale=(1.4, 1.8, 1.1), color=color.rgb(225, 220, 195), position=(0, 3.6, 0.4), rotation=(90, 0, 0))
# Рога
Entity(parent=self.skull, model='cylinder', scale=(0.1, 2.4, 0.1), color=color.rgb(28, 20, 14), position=(-0.7, 0.9, 0), rotation=(0, 0, -42))
Entity(parent=self.skull, model='cylinder', scale=(0.1, 2.4, 0.1), color=color.rgb(28, 20, 14), position=(0.7, 0.9, 0), rotation=(0, 0, 42))
self.hp = 750
self.max_hp = 750
self.speed = 2.4
self.teleport_cd = 7.5
self.root_cd = 4.0
self.slow_timer = 0.0
self.alive = True
def update(self):
if not self.alive:
return
self.teleport_cd -= time.dt
self.root_cd -= time.dt
dist = distance(self.position, player.position)
# Телепортация
if self.teleport_cd <= 0 and dist < 38.0:
self.teleport_cd = 8.5
ang = random.uniform(0, 6.28)
self.position = player.position + Vec3(math.cos(ang) * 9.5, 0, math.sin(ang) * 9.5)
FloatingCombatText(self.position, "*Растворился в тумане*", color.smoke)
# Корни из-под земли
if self.root_cd <= 0 and dist < 32.0:
self.root_cd = 5.0
spawn_root_trap(player.position)
FloatingCombatText(self.position, "ГНЕВ ЗЕМЛИ!", color.orange)
if dist > 3.2:
self.look_at_2d(player.position)
self.position += self.forward * self.speed * time.dt
active_roots = []
def spawn_root_trap(pos):
root = Entity(model='cone', scale=(2.4, 3.6, 2.4), color=color.rgb(45, 65, 30), position=pos, collider='box')
active_roots.append({"entity": root, "timer": 2.2})
drowners_list = [
DrownerEntity(pos=(32, 0, 30)),
DrownerEntity(pos=(38, 0, 36)),
DrownerEntity(pos=(-40, 0, 40))
]
leshen_boss = AncientLeshenBoss(pos=(85, 0, 85))
# =====================================================================
# 9. СЮЖЕТНЫЕ NPC (СТАРОСТА, ТРАВНИЦА, БАРОН)
# =====================================================================
class StoryNPC(Entity):
def __init__(self, pos, name, role):
super().__init__(position=pos)
self.name = name
self.role = role
col = color.rgb(165, 140, 110) if role == "elder" else (color.rgb(45, 120, 75) if role == "herbalist" else color.rgb(120, 35, 45))
self.body = Entity(parent=self, model='cube', scale=(0.85, 1.4, 0.45), color=col, position=(0, 0.7, 0), collider='box')
self.head = Entity(parent=self, model='sphere', scale=0.5, color=color.rgb(215, 185, 155), position=(0, 1.6, 0))
self.quest_icon = Entity(parent=self, model='sphere', scale=0.25, color=color.yellow, position=(0, 2.2, 0))
npc_elder = StoryNPC(pos=(14, 0, 15), name="Староста Богдан", role="elder")
npc_tomira = StoryNPC(pos=(-16, 0, 20), name="Травница Томира", role="herbalist")
npc_baron = StoryNPC(pos=(18, 0, -16), name="Кровавый Барон", role="baron")
# =====================================================================
# 10. КВЕСТОВАЯ БАЗА, БЕСТИАРИЙ И ДАННЫЕ
# =====================================================================
quest_database = {
"leshen": {"name": "Заказ: Древний Хозяин Велена", "stage": 0, "desc": "Староста просит уничтожить Лешего у восточных капищ."},
"herbs": {"name": "Сбор ласточкиной травы", "stage": 0, "desc": "Томире нужны 3 цветка для лечебных отваров."},
"baron": {"name": "Дела во Вроницах", "stage": 0, "desc": "Барон предлагает награду за зачистку окрестностей."}
}
bestiary_text = (
"• УТОПЕЦ (Класс: Трупоеды)\n"
"Слабости: Серебряный меч, Знак Игни (сильно горит), Знак Аард (сбивание с ног).\n\n"
"• ДРЕВНИЙ ЛЕШИЙ (Класс: Реликты)\n"
"Слабости: Серебряный меч, Знак Игни (огромный урон пламенем), Знак Ирден (ловушка замедления).\n\n"
"• РАЗБОЙНИКИ И ДЕЗЕРТИРЫ (Класс: Люди)\n"
"Слабости: Стальной меч, Знак Аксий (внушение и стан)."
)
# =====================================================================
# 11. ИНТЕРФЕЙС, ДИАЛОГОВЫЕ ОКНА И МЕНЮ
# =====================================================================
hud_bg = Entity(parent=camera.ui, model='quad', scale=(0.58, 0.14), position=(-0.58, 0.42), color=color.rgba(20, 20, 20, 200))
hud_text = Text(text="", position=(-0.84, 0.47), scale=1.1, color=color.white)
dialog_window = WindowPanel(
title='Диалог',
content=(
Text(text="", id='d_text'),
Button(text='1. Согласиться / Выполнить', color=color.azure),
Button(text='2. Отказаться', color=color.gray)
),
enabled=False
)
menu_window = WindowPanel(
title='ВЕДЬМАЧИЙ ГРИМУАР',
content=(
Text(text="[1] Инвентарь [2] Бестиарий [3] Дневник Заданий", color=color.gold),
Text(text="", id='m_content'),
Button(text='Закрыть [I / ESC]', color=color.dark_gray)
),
enabled=False
)
menu_current_tab = "INV"
def refresh_menu_content():
if menu_current_tab == "INV":
menu_window.content[1].text = (
f"• Золотые кроны: {player.coins}\n"
f"• Эликсир 'Ласточка': {player.potions} шт. [F]\n"
f"• Ласточкина трава: {player.herbs} шт.\n"
f"• Серебряный меч Волка (Урон +45 по чудовищам)\n"
f"• Стальной меч Новиграда (Урон +35 по людям)"
)
elif menu_current_tab == "BESTIARY":
menu_window.content[1].text = bestiary_text
elif menu_current_tab == "QUESTS":
lines = []
for qid, q in quest_database.items():
st_str = "ВЫПОЛНЕН" if q["stage"] == 3 else ("АКТИВЕН" if q["stage"] > 0 else "НЕ НАЧАТ")
lines.append(f"• {q['name']} — [{st_str}]\n {q['desc']}")
menu_window.content[1].text = "\n\n".join(lines)
# =====================================================================
# 12. ГЛАВНЫЙ ЦИКЛ ОБНОВЛЕНИЯ (UPDATE)
# =====================================================================
def update():
# Регенерация энергии
if player.stamina < player.max_stamina:
player.stamina = min(player.max_stamina, player.stamina + 24 * time.dt)
# Спад токсичности
if player.toxicity > 0:
player.toxicity = max(0.0, player.toxicity - 2.5 * time.dt)
# Обновление камеры
if not menu_window.enabled and not dialog_window.enabled:
cam.update()
# Перемещение Геральта
if not menu_window.enabled and not dialog_window.enabled:
move_vec = Vec3(
(held_keys['d'] - held_keys['a']),
0,
(held_keys['w'] - held_keys['s'])
).normalized()
if move_vec.length() > 0:
player.anim_step += time.dt * 12
rad_y = math.radians(cam.yaw)
fwd = Vec3(math.sin(rad_y), 0, math.cos(rad_y))
rgt = Vec3(math.cos(rad_y), 0, -math.sin(rad_y))
world_move = (fwd * move_vec.z + rgt * move_vec.x).normalized()
spd = player.speed * 0.6 if mouse.right else player.speed
player.position += world_move * spd * time.dt
player.visual.rotation_y = math.degrees(math.atan2(world_move.x, world_move.z))
player.leg_left.rotation_x = math.sin(player.anim_step) * 28
player.leg_right.rotation_x = -math.sin(player.anim_step) * 28
else:
player.leg_left.rotation_x = 0
player.leg_right.rotation_x = 0
# Ведьмачье чутье (ПКМ)
if mouse.right and not menu_window.enabled:
window.color = color.rgb(28, 24, 20)
for d in drowners_list:
if d.alive: d.body.color = color.red
if leshen_boss.alive: leshen_boss.body.color = color.rgb(255, 75, 0)
else:
window.color = color.rgb(115, 145, 175)
for d in drowners_list:
if d.alive: d.body.color = color.rgb(40, 115, 125)
if leshen_boss.alive: leshen_boss.body.color = color.rgb(42, 28, 20)
# Проверка урона от корней Лешего
for r in active_roots[:]:
r["timer"] -= time.dt
if distance(r["entity"].position, player.position) < 2.0 and r["timer"] > 0:
if player.quen_active:
player.quen_active = False
player.quen_sphere.enabled = False
FloatingCombatText(player.position, "КВЕН РАЗБИТ КОРНЯМИ!", color.yellow)
else:
player.hp -= 38 * time.dt
FloatingCombatText(player.position, "-38 HP (Шипы)!", color.red)
if r["timer"] <= 0:
destroy(r["entity"])
active_roots.remove(r)
# Обновление HUD
hud_text.text = (
f"HP: {int(player.hp)}/{player.max_hp} | ВЫНОСЛИВОСТЬ: {int(player.stamina)}/{player.max_stamina}\n"
f"ТОКСИЧНОСТЬ: {int(player.toxicity)}% | МЕЧ [R]: {player.sword}\n"
f"КРОНЫ: {player.coins} | ЛАСТОЧКА [F]: {player.potions} | ТРАВЫ: {player.herbs}"
)
# =====================================================================
# 13. ОБРАБОТКА ВВОДА, БОЯ И ДИАЛОГОВ (INPUT)
# =====================================================================
def input(key):
global menu_current_tab
# Открытие Гримуара [I]
if key == 'i':
menu_window.enabled = not menu_window.enabled
mouse.locked = not menu_window.enabled
refresh_menu_content()
if menu_window.enabled:
if key == '1': menu_current_tab = "INV"; refresh_menu_content()
if key == '2': menu_current_tab = "BESTIARY"; refresh_menu_content()
if key == '3': menu_current_tab = "QUESTS"; refresh_menu_content()
if key == 'escape':
menu_window.enabled = False
mouse.locked = True
return
# Диалоги
if dialog_window.enabled:
if key in ('1', 'e'):
dialog_window.content[1].on_click()
elif key in ('2', 'escape'):
dialog_window.enabled = False
mouse.locked = True
return
# Атака клинком [ЛКМ]
if key == 'left mouse down':
player.attack()
# Урон по Утопцам
for d in drowners_list:
if d.alive and distance(player.position, d.position) < 3.2:
mult = 1.0 if player.sword == "SILVER" else 0.25
dmg = int(38 * mult)
d.hp -= dmg
col = color.white if mult == 1.0 else color.gray
FloatingCombatText(d.position, f"-{dmg} HP" if mult == 1.0 else f"-{dmg} HP (Смени меч [R]!)", col)
if d.hp <= 0:
d.alive = False
d.body.rotation_x = 90
d.y = 0.2
player.coins += 20
FloatingCombatText(d.position, "УТОПЕЦ УБИТ! +20 Крон", color.gold)
# Урон по Лешему
if leshen_boss.alive and distance(player.position, leshen_boss.position) < 4.2:
mult = 1.0 if player.sword == "SILVER" else 0.2
dmg = int(48 * mult)
leshen_boss.hp -= dmg
FloatingCombatText(leshen_boss.position, f"-{dmg} HP", color.gold)
if leshen_boss.hp <= 0:
leshen_boss.alive = False
leshen_boss.body.rotation_x = 90
leshen_boss.y = 0.3
quest_database["leshen"]["stage"] = 2
FloatingCombatText(leshen_boss.position, "ЛЕШИЙ ПОВЕРЖЕН!", color.gold)
# Переключение мечей [R]
if key == 'r':
player.sword = "STEEL" if player.sword == "SILVER" else "SILVER"
col = color.cyan if player.sword == "SILVER" else color.rgb(230, 210, 170)
FloatingCombatText(player.position, f"Обнажен {player.sword} меч", col)
# Эликсир [F]
if key == 'f':
player.drink_potion()
# --- 5 ЗНАКОВ ВЕДЬМАКА ---
# 1. ИГНИ
if key == '1' and player.stamina >= 35:
player.stamina -= 35
FloatingCombatText(player.position, "ЗНАК ИГНИ!", color.orange)
for _ in range(8):
spread = Vec3(random.uniform(-0.25, 0.25), 0, random.uniform(-0.25, 0.25))
FireProjectile(player.position + Vec3(0, 1.4, 0), (player.forward + spread).normalized())
for d in drowners_list:
if d.alive and distance(player.position, d.position) < 9.0:
d.hp -= 45
FloatingCombatText(d.position, "ОЖОГ ИГНИ: -45 HP!", color.orange)
if leshen_boss.alive and distance(player.position, leshen_boss.position) < 11.0:
leshen_boss.hp -= 70
FloatingCombatText(leshen_boss.position, "ЛЕШИЙ ГОРИТ: -70 HP!", color.orange)
# 2. КВЕН
if key == '2' and player.stamina >= 40:
player.stamina -= 40
player.quen_active = True
player.quen_sphere.enabled = True
FloatingCombatText(player.position, "ЗНАК КВЕН!", color.yellow)
# 3. ААРД
if key == '3' and player.stamina >= 35:
player.stamina -= 35
FloatingCombatText(player.position, "ЗНАК ААРД!", color.cyan)
for d in drowners_list:
if d.alive and distance(player.position, d.position) < 8.0:
d.position += player.forward * 4.5
d.hp -= 20
d.stun_timer = 2.0
FloatingCombatText(d.position, "ААРД: СТАН!", color.cyan)
# 4. ИРДЕН
if key == '4' and player.stamina >= 35:
player.stamina -= 35
YrdenTrapEntity(player.position)
FloatingCombatText(player.position, "ЗНАК ИРДЕН!", color.magenta)
for d in drowners_list:
if distance(player.position, d.position) < 9.0:
d.slow_timer = 6.0
if distance(player.position, leshen_boss.position) < 9.0:
leshen_boss.slow_timer = 6.0
# 5. АКСИЙ
if key == '5' and player.stamina >= 35:
player.stamina -= 35
FloatingCombatText(player.position, "ЗНАК АКСИЙ!", color.green)
for d in drowners_list:
if d.alive and distance(player.position, d.position) < 9.0:
d.stun_timer = 4.5
FloatingCombatText(d.position, "ОДУРМАНЕН!", color.green)
# Взаимодействие [E]
if key == 'e':
# Сбор трав и сундуков
for ch in chests_in_world:
if not ch.looted and distance(player.position, ch.position) < 2.5:
ch.loot(player)
for hb in herbs_in_world:
if not hb.picked and distance(player.position, hb.position) < 2.5:
hb.harvest(player)
# Диалоги с NPC
if distance(player.position, npc_elder.position) < 3.8:
open_npc_dialog(npc_elder)
elif distance(player.position, npc_tomira.position) < 3.8:
open_npc_dialog(npc_tomira)
elif distance(player.position, npc_baron.position) < 3.8:
open_npc_dialog(npc_baron)
def open_npc_dialog(npc):
mouse.locked = False
dialog_window.enabled = True
if npc.role == "elder":
st = quest_database["leshen"]["stage"]
if st == 0:
dialog_window.content[0].text = "Староста: Ведьмак, в восточной чаще объявился Леший!\nУбей его, и 300 крон твои."
def accept():
quest_database["leshen"]["stage"] = 1
dialog_window.enabled = False
mouse.locked = True
FloatingCombatText(player.position, "ЗАКАЗ ВЗЯТ!", color.gold)
dialog_window.content[1].text = "1. Беру заказ на чудище."
dialog_window.content[1].on_click = accept
elif st == 2:
dialog_window.content[0].text = "Староста: Хвала богам, ты сразил тварь!\nВот твоя награда."
def finish():
quest_database["leshen"]["stage"] = 3
player.coins += 300
npc.quest_icon.enabled = False
dialog_window.enabled = False
mouse.locked = True
FloatingCombatText(player.position, "+300 КРОН!", color.gold)
dialog_window.content[1].text = "1. Забрать награду (300 крон)."
dialog_window.content[1].on_click = finish
else:
dialog_window.content[0].text = "Староста: Ступай на восток к капищам и сокруши Лешего!"
def close():
dialog_window.enabled = False
mouse.locked = True
dialog_window.content[1].text = "1. Уже иду."
dialog_window.content[1].on_click = close
elif npc.role == "herbalist":
dialog_window.content[0].text = "Томира: Здравствуй, Геральт. Могу сварить 'Ласточку' за 40 крон."
def buy():
if player.coins >= 40:
player.coins -= 40
player.potions += 1
FloatingCombatText(player.position, "+1 Ласточка куплена", color.green)
dialog_window.enabled = False
mouse.locked = True
dialog_window.content[1].text = "1. Купить 'Ласточку' (-40 крон)"
dialog_window.content[1].on_click = buy
elif npc.role == "baron":
dialog_window.content[0].text = "Барон: Ведьмак, держи руку на рукояти меча. Время нынче темное."
def close():
dialog_window.enabled = False
mouse.locked = True
dialog_window.content[1].text = "1. Бывай, Барон."
dialog_window.content[1].on_click = close
# =====================================================================
# 14. ЗАПУСК ИГРОВОГО ДВИЖКА
# =====================================================================
app.run()