using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class EchoTrigger : MonoBehaviour
{
[Header("Связь с шейдером")]
public Material echoMaterial;
[Header("Синхронизация с шагами персонажа")]
[Tooltip("Включить синхронизацию волн эхолокации с анимацией шагов")]
public bool syncWithAnimation = true;
public Animator playerAnimator;
public CharacterController characterController;
[Tooltip("Позиционировать волну строго на кости наступающей ноги (L/R)")]
public bool spawnAtFootBone = true;
public Transform leftFootBone;
public Transform rightFootBone;
[Header("Фазы касания земли в анимации (0.0 - 1.0)")]
[Tooltip("Фаза шага левой ногой при обычной ходьбе")]
[Range(0f, 1f)] public float leftFootPhaseWalk = 0.08f;
[Tooltip("Фаза шага правой ногой при обычной ходьбе")]
[Range(0f, 1f)] public float rightFootPhaseWalk = 0.55f;
[Tooltip("Фаза шага левой ногой при спринте")]
[Range(0f, 1f)] public float leftFootPhaseSprint = 0.12f;
[Tooltip("Фаза шага правой ногой при спринте")]
[Range(0f, 1f)] public float rightFootPhaseSprint = 0.62f;
[Header("Резервный режим (по дистанции)")]
[Tooltip("Используется только если аниматор не найден или выключена синхронизация")]
public float stepDistance = 1.5f;
public float waveSpeed = 15f;
[Header("Радиусы звука (Шум)")]
public float sneakRadius = 5f;
public float walkRadius = 15f;
public float runRadius = 30f;
[Header("Яркость волны (Интенсивность)")]
public float sneakIntensity = 0.3f; // Тусклый свет для стелса
public float walkIntensity = 1.0f; // Обычный свет для шага
public float runIntensity = 3.0f; // Очень яркая вспышка для бега
[Header("Звуки шагов (Опционально)")]
public AudioSource audioSource;
public AudioClip[] footstepSounds;
[Range(0f, 1f)] public float footstepVolume = 0.6f;
[Header("Реакция мутантов")]
public bool alertMutants = true;
[Header("Акустическое ослепление (Босс)")]
private float blindnessTimer = 0f;
public bool IsBlinded => blindnessTimer > 0f;
///
/// Полностью глушит эхолокацию: шум шагов, выстрелы и броски не создают волн
///
public void ApplyBlindness(float duration)
{
blindnessTimer = Mathf.Max(blindnessTimer, duration);
for (int i = 0; i < MAX_WAVES; i++)
{
waveActive[i] = false;
SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), 0f);
SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f);
}
}
// Пул из 3 одновременных волн
private const int MAX_WAVES = 3;
private float[] waveRadii = new float[MAX_WAVES];
private float[] waveMaxRadii = new float[MAX_WAVES];
private float[] waveIntensities = new float[MAX_WAVES];
private bool[] waveActive = new bool[MAX_WAVES];
public static EchoTrigger Instance;
private Vector3 lastPosition;
private float distanceMoved = 0f;
private bool wasMoving = false;
private int lastLeftStepCycle = int.MinValue;
private int lastRightStepCycle = int.MinValue;
private PlayerNoise playerNoise;
private readonly List allEchoMaterials = new List();
void Awake()
{
if (Instance == null) Instance = this;
}
///
/// Активна ли прямо сейчас хоть одна световая волна эхолокации (отрисовывается ли шейдер)
///
public bool HasActiveWave
{
get
{
for (int i = 0; i < MAX_WAVES; i++)
{
if (waveActive[i] && waveRadii[i] > 0.05f) return true;
}
return false;
}
}
///
/// Издает ли игрок шум или движется ли он прямо сейчас (шаги, клавиши, активные волны)
///
public bool IsPlayerMakingNoise()
{
// 1. Отрисовывается ли прямо сейчас волна эхолокации в шейдере
if (HasActiveWave) return true;
// 2. Движется ли контроллер персонажа
if (characterController != null && characterController.velocity.sqrMagnitude > 0.05f)
return true;
// 3. Активно ли перемещение в аниматоре
if (playerAnimator != null && playerAnimator.GetBool("IsMoving"))
return true;
// 4. Зажаты ли клавиши перемещения
var keyboard = Keyboard.current;
if (keyboard != null && (keyboard.wKey.isPressed || keyboard.aKey.isPressed || keyboard.sKey.isPressed || keyboard.dKey.isPressed))
return true;
return false;
}
void Start()
{
CollectAllEchoMaterials();
for (int i = 0; i < MAX_WAVES; i++)
{
SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), 0f);
SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f);
SetVectorOnAllEchoMaterials("_EchoPosition" + (i + 1), Vector4.zero);
}
lastPosition = transform.position;
if (characterController == null)
characterController = GetComponent();
if (playerAnimator == null)
playerAnimator = GetComponentInChildren();
if (playerNoise == null)
playerNoise = GetComponent();
if (audioSource == null)
audioSource = GetComponent();
FindFootBones();
// Начальная ознакомительная волна при старте сцены, чтобы игрок сразу увидел комнату
Vector3 startWavePos = transform.position;
if (characterController != null)
{
float floorY = transform.position.y + characterController.center.y - (characterController.height * 0.5f);
startWavePos.y = floorY + 0.05f;
}
TriggerExternalWave(startWavePos, walkRadius, 1.0f);
}
public void CollectAllEchoMaterials()
{
allEchoMaterials.Clear();
if (echoMaterial != null && !allEchoMaterials.Contains(echoMaterial))
allEchoMaterials.Add(echoMaterial);
Renderer[] allRenderers = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var r in allRenderers)
{
if (r == null || r.sharedMaterials == null) continue;
foreach (var m in r.sharedMaterials)
{
if (m != null && m.shader != null && (m.shader.name == "Custom/EchoCloudShader" || m.shader.name == "Custom/EchoCloudShader_Glass"))
{
if (!allEchoMaterials.Contains(m))
allEchoMaterials.Add(m);
}
}
}
}
public void SetFloatOnAllEchoMaterials(string propName, float value)
{
Shader.SetGlobalFloat(propName, value);
for (int i = 0; i < allEchoMaterials.Count; i++)
{
if (allEchoMaterials[i] != null)
allEchoMaterials[i].SetFloat(propName, value);
}
}
public void SetVectorOnAllEchoMaterials(string propName, Vector4 value)
{
Shader.SetGlobalVector(propName, value);
for (int i = 0; i < allEchoMaterials.Count; i++)
{
if (allEchoMaterials[i] != null)
allEchoMaterials[i].SetVector(propName, value);
}
}
public void FindFootBones()
{
if (playerAnimator == null) return;
if (playerAnimator.isHuman)
{
if (leftFootBone == null)
leftFootBone = playerAnimator.GetBoneTransform(HumanBodyBones.LeftFoot);
if (rightFootBone == null)
rightFootBone = playerAnimator.GetBoneTransform(HumanBodyBones.RightFoot);
}
// Резервный поиск по имени, если аватар Generic или кости не замаплены
if (leftFootBone == null || rightFootBone == null)
{
Transform[] allChildren = playerAnimator.GetComponentsInChildren();
foreach (var t in allChildren)
{
string lower = t.name.ToLower();
if (leftFootBone == null && (lower.Contains("l_foot") || lower.Contains("foot_l") || lower.Contains("leftfoot")))
leftFootBone = t;
if (rightFootBone == null && (lower.Contains("r_foot") || lower.Contains("foot_r") || lower.Contains("rightfoot")))
rightFootBone = t;
}
}
}
void Update()
{
if (blindnessTimer > 0f)
{
blindnessTimer -= Time.deltaTime;
for (int i = 0; i < MAX_WAVES; i++)
{
SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f);
}
}
HandleStepDetection();
HandleManualEchoPulse();
UpdateWaveShaderProgress();
}
void HandleManualEchoPulse()
{
if (IsBlinded) return;
// Возможность выпустить эхолокационный импульс (Space, Q или ПКМ)
var keyboard = Keyboard.current;
var mouse = Mouse.current;
bool pulseKey = (keyboard != null && (keyboard.spaceKey.wasPressedThisFrame || keyboard.qKey.wasPressedThisFrame))
|| (mouse != null && mouse.rightButton.wasPressedThisFrame);
if (pulseKey)
{
Vector3 origin = transform.position;
if (characterController != null)
{
origin.y = transform.position.y + characterController.center.y - (characterController.height * 0.5f) + 0.05f;
}
TriggerExternalWave(origin, walkRadius * 1.2f, 1.2f);
}
}
void HandleStepDetection()
{
// Всегда отслеживаем физическое перемещение
Vector3 currentPos = new Vector3(transform.position.x, 0, transform.position.z);
Vector3 lastPos = new Vector3(lastPosition.x, 0, lastPosition.z);
float delta = Vector3.Distance(currentPos, lastPos);
distanceMoved += delta;
lastPosition = transform.position;
bool stepTriggeredByAnimation = false;
if (syncWithAnimation && playerAnimator != null)
{
stepTriggeredByAnimation = CheckAnimationSteps();
}
// Если шаг был по анимации — сбрасываем счетчик дистанции
if (stepTriggeredByAnimation)
{
distanceMoved = 0f;
}
// Резерв: если по какой-то причине аниматор не сработал за stepDistance метров движения
else if (distanceMoved >= stepDistance)
{
distanceMoved = 0f;
TriggerFootstep(true);
}
}
bool CheckAnimationSteps()
{
AnimatorStateInfo stateInfo = playerAnimator.GetCurrentAnimatorStateInfo(0);
bool isMoving = playerAnimator.GetBool("IsMoving");
float animSpeed = playerAnimator.GetFloat("Speed");
bool inLocomotion = stateInfo.IsName("Locomotion") || playerAnimator.IsInTransition(0);
if (isMoving && animSpeed > 0.05f && inLocomotion)
{
float speedRatio = Mathf.InverseLerp(1.5f, 6.0f, animSpeed);
float targetLeftPhase = Mathf.Lerp(leftFootPhaseWalk, leftFootPhaseSprint, speedRatio);
float targetRightPhase = Mathf.Lerp(rightFootPhaseWalk, rightFootPhaseSprint, speedRatio);
float currTime = stateInfo.normalizedTime;
if (!wasMoving)
{
lastLeftStepCycle = Mathf.FloorToInt(currTime - targetLeftPhase);
lastRightStepCycle = Mathf.FloorToInt(currTime - targetRightPhase);
wasMoving = true;
return false;
}
bool stepped = false;
int leftCycle = Mathf.FloorToInt(currTime - targetLeftPhase);
if (leftCycle > lastLeftStepCycle)
{
lastLeftStepCycle = leftCycle;
TriggerFootstep(true);
stepped = true;
}
int rightCycle = Mathf.FloorToInt(currTime - targetRightPhase);
if (rightCycle > lastRightStepCycle)
{
lastRightStepCycle = rightCycle;
TriggerFootstep(false);
stepped = true;
}
return stepped;
}
else
{
wasMoving = false;
return false;
}
}
public void TriggerStepWave()
{
TriggerFootstep(true);
}
void TriggerFootstep(bool isLeftFoot)
{
Vector3 spawnPos = transform.position;
if (spawnAtFootBone)
{
Transform foot = isLeftFoot ? leftFootBone : rightFootBone;
if (foot != null)
{
spawnPos = foot.position;
}
}
// Привязываем высоту волны точно к уровню пола под персонажем
if (characterController != null)
{
float floorY = transform.position.y + characterController.center.y - (characterController.height * 0.5f);
spawnPos.y = floorY + 0.05f;
}
// Определяем режим передвижения (Sneak / Walk / Run) через InputSystem
var keyboard = Keyboard.current;
bool isRunning = keyboard != null && keyboard.leftShiftKey.isPressed;
bool isSneaking = keyboard != null && (keyboard.leftCtrlKey.isPressed || keyboard.cKey.isPressed);
if (playerAnimator != null)
{
float speedVal = playerAnimator.GetFloat("Speed");
if (speedVal > 4.5f) isRunning = true;
else if (speedVal > 0f && speedVal < 2.0f && !isRunning) isSneaking = true;
}
float targetMax = walkRadius;
float targetIntensity = walkIntensity;
if (isRunning)
{
targetMax = runRadius;
targetIntensity = runIntensity;
}
else if (isSneaking)
{
targetMax = sneakRadius;
targetIntensity = sneakIntensity;
}
// Запуск световой волны эхолокации в шейдере
TriggerExternalWave(spawnPos, targetMax, targetIntensity);
// Воспроизведение звука шага (если задан)
if (audioSource != null && footstepSounds != null && footstepSounds.Length > 0)
{
AudioClip clip = footstepSounds[Random.Range(0, footstepSounds.Length)];
if (clip != null)
{
float vol = isSneaking ? footstepVolume * 0.4f : (isRunning ? footstepVolume * 1.3f : footstepVolume);
audioSource.PlayOneShot(clip, vol);
}
}
// Синхронизация с PlayerNoise или прямое оповещение мутантов
if (playerNoise != null)
{
playerNoise.EmitFootstep(spawnPos, targetMax);
}
else if (alertMutants)
{
Collider[] colliders = Physics.OverlapSphere(spawnPos, targetMax);
foreach (Collider hit in colliders)
{
BlindMutantAI mutant = hit.GetComponent();
if (mutant != null) mutant.HearSound(spawnPos);
}
}
}
void UpdateWaveShaderProgress()
{
if (allEchoMaterials.Count == 0)
{
CollectAllEchoMaterials();
if (allEchoMaterials.Count == 0 && echoMaterial == null) return;
}
for (int i = 0; i < MAX_WAVES; i++)
{
if (waveActive[i])
{
waveRadii[i] += waveSpeed * Time.deltaTime;
float currentRadius = waveRadii[i];
float maxRadius = waveMaxRadii[i];
float progress = Mathf.Clamp01(currentRadius / maxRadius);
// Быстрое появление: уже к 0.4м (за 0.025с) волна на 100% яркости у ног
float fadeIn = Mathf.Clamp01(currentRadius / 0.4f);
// Плавное рассеивание к концу дистанции
float fadeOut = Mathf.Pow(1f - progress, 1.2f);
float opacity = (fadeIn * fadeOut) * waveIntensities[i];
opacity = Mathf.Max(0f, opacity);
SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), currentRadius);
SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), opacity);
if (currentRadius >= maxRadius)
{
waveActive[i] = false;
SetFloatOnAllEchoMaterials("_EchoRadius" + (i + 1), 0f);
SetFloatOnAllEchoMaterials("_EchoOpacity" + (i + 1), 0f);
}
}
}
}
public void TriggerExternalWave(Vector3 position, float targetMax, float targetIntensity)
{
if (IsBlinded) return;
if (allEchoMaterials.Count == 0)
{
CollectAllEchoMaterials();
if (allEchoMaterials.Count == 0 && echoMaterial == null) return;
}
// Ищем свободный слот
int waveIndex = -1;
for (int i = 0; i < MAX_WAVES; i++)
{
if (!waveActive[i])
{
waveIndex = i;
break;
}
}
// Если все слоты заняты, берем самую старую / дальнюю волну
if (waveIndex == -1)
{
float maxRad = -1f;
for (int i = 0; i < MAX_WAVES; i++)
{
if (waveRadii[i] > maxRad)
{
maxRad = waveRadii[i];
waveIndex = i;
}
}
}
// Назначаем волне её радиус и яркость
waveMaxRadii[waveIndex] = targetMax;
waveIntensities[waveIndex] = targetIntensity;
waveRadii[waveIndex] = 0.05f; // Начинаем чуть выше 0, чтобы сразу вступала в силу
waveActive[waveIndex] = true;
// Начальная установка координат и стартовой прозрачности
SetFloatOnAllEchoMaterials("_EchoRadius" + (waveIndex + 1), 0.05f);
SetFloatOnAllEchoMaterials("_EchoOpacity" + (waveIndex + 1), 0.2f * targetIntensity);
SetVectorOnAllEchoMaterials("_EchoPosition" + (waveIndex + 1), new Vector4(position.x, position.y, position.z, 1.0f));
}
}