Files
CybelovZOVod/addons/NPC/script/enemy_mob.gd
T
2026-07-21 00:27:17 +07:00

225 lines
8.5 KiB
GDScript

extends CharacterBody3D
@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()
# --- ФИЗИКА, ПРИЦЕЛИВАНИЕ И СТРЕЛЬБА ---
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)
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)