226 lines
8.6 KiB
GDScript
226 lines
8.6 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()
|
|
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)
|
|
|
|
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)
|