extends CharacterBody3D @onready var anim_player = $AnimationPlayer # Убедись, что имя узла в дереве сцены совпадает буква в букву! @export var move_speed: float = 4.0 # Скорость преследования игрока @export var damage_to_player: float = 10.0 # Базовый урон за один выстрел @export var attack_cooldown: float = 1.0 # Интервал стрельбы ПОСЛЕ первого выстрела (в секундах) @export var vision_range: float = 25.0 # Радиус чутья моба (в метрах) # --- НАСТРОЙКА ЗАДЕРЖКИ ПЕРЕД ВЫСТРЕЛОМ --- @export var lock_on_time: float = 2.5 # Время на прицеливание перед выстрелом (в секундах) var aiming_timer: float = 0.0 # Текущее накопленное время прицеливания # --- НАСТРОЙКА УНИКАЛЬНОЙ МУЗЫКИ ДЛЯ МОБА --- @export var custom_battle_music : AudioStream # --- 🖼 ВЫБОР КАРТИНКИ ВСПЫШКИ В ИНСПЕКТОРЕ --- @export var muzzle_flash_texture : Texture2D # Переменные здоровья (работают напрямую, как у мишени) @export var health: float = 100.0 var isDisabled: bool = false var player_node: Node = null var is_player_in_range: bool = false var is_currently_seeing_player: bool = false # Динамический узел вспышки, который код создаст сам var dynamic_flash_sprite : Sprite3D # Таймер перезарядки оружия var attack_timer: float = 0.0 # Сила гравитации var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") func _ready() -> void: if not is_in_group("Enemies"): add_to_group("Enemies") print("[ВРАГ] Тактический моб со зрением через укрытия запущен. Здоровье: ", health) # АВТО-СОЗДАНИЕ И НАСТРОЙКА КАРТИНКИ ВСПЫШКИ if muzzle_flash_texture != null: dynamic_flash_sprite = Sprite3D.new() dynamic_flash_sprite.texture = muzzle_flash_texture dynamic_flash_sprite.visible = false dynamic_flash_sprite.billboard = BaseMaterial3D.BILLBOARD_ENABLED # Поворот на игрока # Пытаемся прикрепить к кончику ствола оружия, если есть контейнер, иначе к центру моба if has_node("WeaponManager/WeaponContainer"): get_node("WeaponManager/WeaponContainer").add_child(dynamic_flash_sprite) # Сдвигаем чуть вперед по оси Z, чтобы картинка не тонула в дуле dynamic_flash_sprite.position = Vector3(0, 0, -0.4) else: add_child(dynamic_flash_sprite) dynamic_flash_sprite.position = Vector3(0, 1.2, -0.5) # ИНИЦИАЛИЗАЦИЯ ОРУЖИЯ ДЛЯ АНИМАЦИЙ ПРИ СТАРТЕ if has_node("WeaponManager/AnimationManager") and has_node("WeaponManager/WeaponContainer"): var anim_mgr = get_node("WeaponManager/AnimationManager") var weapon_model = get_node("WeaponManager/WeaponContainer") var mock_weapon = { "tiltRotAmount": 0.05, "tiltRotSpeed": 5.0, "bobFreq": 0.005, "bobAmount": 0.05, "bobSpeed": 5.0, "onIdleBobFreqDivider": 2.0, "bobPos": Vector3.ZERO } anim_mgr.getCurrentWeapon(mock_weapon, weapon_model) # --- СИСТЕМА ПОПАДАНИЙ КАК У МИШЕНИ --- func hitscanHit(damageVal: float, _hitscanDir: Vector3, _hitscanPos: Vector3) -> void: if isDisabled: return health -= damageVal print("[ВРАГ] Хитскан попал в моба! Здоровье моба: ", health) if health <= 0.0: die() func projectileHit(damageVal: float, _hitscanDir: Vector3) -> void: if isDisabled: return health -= damageVal print("[ВРАГ] Прожектайл попал в моба! Здоровье моба: ", health) if health <= 0.0: die() func die() -> void: isDisabled = true print("[ВРАГ] Моб уничтожен!") if custom_battle_music and is_currently_seeing_player and has_node("/root/MusicManager"): get_node("/root/MusicManager").unregister_enemy_vision(self, custom_battle_music) queue_free() if LevelStats: LevelStats.register_kill() # --- ФИЗИКА, ПРИЦЕЛИВАНИЕ И СТРЕЛЬБА --- func _physics_process(delta: float) -> void: if isDisabled: return if not is_on_floor(): velocity.y -= gravity * delta else: velocity.y = 0 if attack_timer > 0: attack_timer -= delta if player_node == null: player_node = get_tree().current_scene.find_child("PlayerCharacter", true, false) if player_node != null: var distance = global_position.distance_to(player_node.global_position) var is_player_alive = true var p_health = player_node.find_child("HealthComponent", true, false) if p_health and "current_health" in p_health: is_player_alive = p_health.current_health > 0 elif "health" in player_node: is_player_alive = player_node.health > 0 if distance <= vision_range and is_player_alive: is_player_in_range = true else: is_player_in_range = false else: is_player_in_range = false if is_player_in_range and player_node != null: var distance = global_position.distance_to(player_node.global_position) var target_pos = player_node.global_position target_pos.y = global_position.y if global_position.distance_to(target_pos) > 0.1: look_at(target_pos, Vector3.UP) if distance > 2.5: var direction = (player_node.global_position - global_position).normalized() velocity.x = direction.x * move_speed velocity.z = direction.z * move_speed else: velocity.x = 0 velocity.z = 0 var sees_now = has_clear_line_of_sight() if custom_battle_music: if sees_now != is_currently_seeing_player: is_currently_seeing_player = sees_now if has_node("/root/MusicManager"): if is_currently_seeing_player: get_node("/root/MusicManager").register_enemy_vision(self, custom_battle_music) else: get_node("/root/MusicManager").unregister_enemy_vision(self, custom_battle_music) if sees_now: if attack_timer <= 0: aiming_timer += delta if aiming_timer >= lock_on_time: enemy_shoot() aiming_timer = 0.0 else: aiming_timer = 0.0 else: velocity.x = 0 velocity.z = 0 aiming_timer = 0.0 if custom_battle_music and is_currently_seeing_player: is_currently_seeing_player = false if has_node("/root/MusicManager"): get_node("/root/MusicManager").unregister_enemy_vision(self, custom_battle_music) # === ИСПРАВЛЕННЫЙ И НЕУБИВАЕМЫЙ БЛОК АНИМАЦИЙ ДЛЯ UAL === if anim_player != null: # Высчитываем дистанцию до игрока для точного переключения стейтов var distance_to_p = 999.0 if player_node != null: distance_to_p = global_position.distance_to(player_node.global_position) # Моб бежит, если игрок в зоне видимости и расстояние больше дистанции атаки (2.5м) var is_moving_now = is_player_in_range and distance_to_p > 2.5 if is_moving_now: # Если анимация бега еще не запущена или плеер полностью остановился if anim_player.assigned_animation != "Jog_Fwd" or not anim_player.is_playing(): anim_player.play("Jog_Fwd", 0.2) # 0.2 — плавное сглаживание переходов костей else: # Если моб стоит на месте (в укрытии, стреляет или потерял игрока) # Проверяем, чтобы покой не сбивал анимацию удара/выстрела Punch_Jab if anim_player.assigned_animation != "Idle" and anim_player.assigned_animation != "Punch_Jab": if not anim_player.is_playing() or anim_player.assigned_animation == "Jog_Fwd": anim_player.play("Idle", 0.2) # ======================================================= move_and_slide() func has_clear_line_of_sight() -> bool: if player_node == null: return false var start_pos = global_position + Vector3(0, 1.0, 0) var end_pos = player_node.global_position + Vector3(0, 1.0, 0) var space_state = get_world_3d().direct_space_state var query = PhysicsRayQueryParameters3D.create(start_pos, end_pos) var exclusion_list = [get_rid()] for child in get_children(): if child is CollisionObject3D: exclusion_list.append(child.get_rid()) query.exclude = exclusion_list query.collide_with_bodies = true query.collide_with_areas = false var result = space_state.intersect_ray(query) if result.is_empty(): return true var hit_node = result.collider if hit_node == player_node or hit_node.get_parent() == player_node or hit_node.is_in_group("Player"): return true return false func enemy_shoot() -> void: attack_timer = attack_cooldown if player_node == null: return # --- 🔊 ВОСПРОИЗВЕДЕНИЕ ЗВУКА ВЫСТРЕЛА --- var shoot_audio = get_node_or_null("%ShootSound") if shoot_audio == null: shoot_audio = find_child("ShootSound", true, false) if shoot_audio: shoot_audio.stop() shoot_audio.play() # --- 🔥 ОТОБРАЖЕНИЕ КАРТИНКИ ВСПЫШКИ ВСПЛЫВАЮЩЕЙ НА ДОЛЮ СЕКУНДЫ --- if is_instance_valid(dynamic_flash_sprite): dynamic_flash_sprite.rotation.z = randf_range(0, TAU) # Каждый раз разный угол картинки dynamic_flash_sprite.visible = true get_tree().create_timer(0.06).timeout.connect(func(): if is_instance_valid(dynamic_flash_sprite): dynamic_flash_sprite.visible = false ) # --- ЗАПУСКАЕМ АНИМАЦИЮ ВЫСТРЕЛА ТЕЛА --- if has_node("AnimationPlayer"): var anim = get_node("AnimationPlayer") anim.stop() anim.play("ShootAnimPistol") # --- НАНЕСЕНИЕ УРОНА ИГРОКУ --- var player_health = player_node.find_child("HealthComponent", true, false) if player_health and "take_damage" in player_health: var reduced_damage = damage_to_player * 0.8 print("[ВРАГ] Бах! Вспышка отрисована. Нанесено урона: ", reduced_damage) player_health.take_damage(reduced_damage)