using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Rendering;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace AibisDream.FixSystem
{
[RequireComponent(typeof(Slider))]
public class MemorySlider : MonoBehaviour
{
[SerializeField] private float defaultValue = 0f;
[SerializeField] private float sliderMaxValue = 6f;
[SerializeField] private float lockThreshold = 0.08f;
[SerializeField] private float nearThreshold = 0.24f;
[SerializeField, Range(0f, 1f)] private float unresolvedVisualFloor = 0.12f;
[SerializeField] private TimelineAsset openAsset;
[SerializeField] private TimelineAsset closeAsset;
private Slider slider;
private PlayableDirector playableDirector;
private Volume _volume;
private bool driveVolume = true;
/// 视觉 Lock/Unlock(Timeline);true = 收起未开,false = 已打开。
private bool isLocked;
public float TargetValue { get; set; }
public float StartValue => defaultValue;
public float MaxValue => sliderMaxValue;
public bool IsFound { get; private set; }
public event Action OnFound;
public event Action OnLost;
public event Action OnTuningChanged;
public void Init(Volume volume)
{
slider = GetComponent();
playableDirector = GetComponent();
_volume = volume;
driveVolume = true;
IsFound = false;
slider.onValueChanged.RemoveAllListeners();
slider.onValueChanged.AddListener(HandleValueChanged);
slider.maxValue = sliderMaxValue;
slider.value = defaultValue;
UpdateTuningOutput(slider.value);
isLocked = true;
SetInteractable(false);
}
/// 仅控制是否可拖动,与 / 无关。
public void SetInteractable(bool interactable) => slider.interactable = interactable;
public void SetVolumeOutputEnabled(bool enabled)
{
driveVolume = enabled;
if (!driveVolume && _volume != null)
_volume.weight = 0f;
}
///
/// 将拉杆视觉复位到默认位置并清零调谐输出(不播 Timeline、不触发搜索判定)。
/// 用于记忆播放结束后回到待机界面。
///
public void ResetVisualToDefault()
{
IsFound = false;
if (slider == null)
slider = GetComponent();
slider.SetValueWithoutNotify(defaultValue);
if (_volume != null && driveVolume)
_volume.weight = 0f;
OnTuningChanged?.Invoke(0f);
}
///
/// 新一轮搜索前复位滑条与 IsFound(不播 Timeline、不改变视觉锁状态)。
/// 需先设置好 。
///
public void PrepareForSearch()
{
slider.value = defaultValue;
HandleValueChanged(slider.value);
}
/// 仅播放打开 Timeline(若有),不碰 interactable。
public void UnlockSlider(Action onComplete = null)
{
if (!isLocked)
{
onComplete?.Invoke();
return;
}
PlayTimeline(openAsset, () =>
{
isLocked = false;
onComplete?.Invoke();
});
}
/// 仅播放收起 Timeline(若有),不碰 interactable。
public void LockSlider(Action onComplete = null)
{
if (isLocked)
{
onComplete?.Invoke();
return;
}
slider.value = 0f;
UpdateTuningOutput(0f);
PlayTimeline(closeAsset, () =>
{
isLocked = true;
onComplete?.Invoke();
});
}
private void HandleValueChanged(float value)
{
float t = UpdateTuningOutput(value);
bool wasFound = IsFound;
IsFound = t < lockThreshold;
if (IsFound && !wasFound)
OnFound?.Invoke();
else if (!IsFound && wasFound)
OnLost?.Invoke();
}
private float UpdateTuningOutput(float value)
{
float t = GetNormalizedDistance(value);
float visualStrength = GetVisualStrength(t);
if (_volume != null && driveVolume)
_volume.weight = Mathf.Lerp(0, 1, visualStrength);
OnTuningChanged?.Invoke(visualStrength);
return t;
}
private float GetNormalizedDistance(float value)
{
float distance = Mathf.Abs(value - TargetValue);
float maxDistance = Mathf.Max(
Mathf.Abs(slider.maxValue - TargetValue),
Mathf.Abs(slider.minValue - TargetValue)
);
return Mathf.Clamp01(distance / Mathf.Max(0.0001f, maxDistance));
}
private float GetVisualStrength(float t)
{
float lockBand = Mathf.Clamp01(lockThreshold);
float nearBand = Mathf.Clamp(Mathf.Max(nearThreshold, lockBand + 0.001f), lockBand + 0.001f, 1f);
if (t <= lockBand)
return 0f;
if (t < nearBand)
{
float nearProgress = Mathf.InverseLerp(lockBand, nearBand, t);
float easedProgress = Mathf.SmoothStep(0f, 1f, nearProgress);
return Mathf.Lerp(unresolvedVisualFloor, nearBand, easedProgress);
}
return t;
}
private void PlayTimeline(TimelineAsset asset, Action onComplete)
{
if (asset == null || playableDirector == null)
{
onComplete?.Invoke();
return;
}
playableDirector.Stop();
playableDirector.playableAsset = asset;
playableDirector.time = 0;
playableDirector.extrapolationMode = DirectorWrapMode.None;
Action callback = null;
callback = pd =>
{
playableDirector.stopped -= callback;
onComplete?.Invoke();
};
playableDirector.stopped += callback;
playableDirector.Play();
}
}
}