using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using DG.Tweening; using Yarn.Unity; using System.Linq; using UnityEngine.UI; using AibisDream.FixSystem; using AibisDream.Framework; using AibisDream.Kit; using AibisDream.SaveSystem; using UnityEngine.ResourceManagement.AsyncOperations; using Sequence = DG.Tweening.Sequence; namespace AibisDream { public class EyeSystem : MonoBehaviour { private const string MemorySpritePath = "Sprite/Memory/{0}.png"; [SerializeField] private List targets = new(); public List Targets => targets; private EyeTarget currentTarget; public EyeTarget CurrentTarget => currentTarget; [Header("Memory Overlay (UI)")] public Image memoryRender; public GameObject eyeView; [Header("Focus Pan Bounds")] [SerializeField] private Vector2 focusMinBounds = new(30.2f, -4.5f); [SerializeField] private Vector2 focusMaxBounds = new(50.2f, 5f); [Header("Visual Tuning")] [SerializeField] private EyeVisualSettings visualSettings = new(); [SerializeField] private EyeViewportOverlay viewportOverlay; [Tooltip("Play 模式下改 Visual Tuning 后,自动对当前目标重播一次 Focused 效果(如 ColorIn)。")] [SerializeField] private bool replayFocusedEffectOnSettingsChange = true; public EyeVisualSettings VisualSettings => visualSettings; private IEyeStateEffect eyeStateEffect; private EyeColorState _currentState = EyeColorState.CannotImagineColor; private MouseFollowAndZoom mouseFollowAndZoom; private Sequence glitchin; public EyeColorState GetCurrentColorState() => _currentState; /// Eye/UF 演出倍速(默认见 Visual Tuning → Presentation Speed)。 public float PresentationSpeed => Mathf.Max(0.01f, visualSettings != null ? visualSettings.presentationSpeed : 1f); public float ScaleDuration(float duration) => duration / PresentationSpeed; public float GetEffectTransitionDuration() => ScaleDuration(visualSettings.GetDurationForState(_currentState)); private void PlayPresentationSfx(string eventName) { AudioManager.Instance.PlaySfx(eventName, pitch: PresentationSpeed); } /// 仅「可想象色彩」上色时眨眼;无 autoBlink / 失焦眨眼。 private bool ShouldBlinkForImagine() { return _currentState == EyeColorState.CanImagineColor; } private IEnumerator WaitForFocusMovement(float moveDuration) { if (!EnsureMouseFollowAndZoom()) { yield break; } yield return mouseFollowAndZoom.WaitForFocusMove(moveDuration); } private IEnumerator RunFocusedEffectWithPreBlink(EyeTarget target) { if (EnsureMouseFollowAndZoom()) { yield return mouseFollowAndZoom.WaitForFocusMove(); } float duration = GetEffectTransitionDuration(); EyeViewportOverlay overlay = EnsureViewportOverlay(); if (overlay != null && Application.isPlaying && ShouldBlinkForImagine()) { yield return overlay.PlayPreEffectBlinkThen( 2, visualSettings, () => eyeStateEffect?.ApplyFocusedEffect(target, this), visualSettings.imagineEffectAtBlinkOpen, visualSettings.imagineBlinkGap, PresentationSpeed); } else { eyeStateEffect?.ApplyFocusedEffect(target, this); } yield return new WaitForSeconds(duration); } private IEnumerator RunBlurOut(EyeTarget target, float duration) { duration = ScaleDuration(duration); if (target != null) { target.BlurOut(duration); } yield return new WaitForSeconds(duration); } /// /// 换目标时的失焦:只做效果,不眨眼(眨眼仅在想象上色 FadeIn)。 /// CanImagineColor 下若颜色已是失焦态,仍需补 BlurIn,否则未上色过的目标会一直保持清晰。 /// private void ApplyUnfocusedEffectSilent(EyeTarget target) { if (target == null || eyeStateEffect == null) { return; } if (_currentState == EyeColorState.CanImagineColor && target.IsAtUnfocusedColorState(visualSettings)) { target.BlurIn(GetEffectTransitionDuration()); return; } eyeStateEffect.ApplyUnfocusedEffect(target, this); } private IEnumerator RunUnfocusedEffect(EyeTarget target) { if (target == null || eyeStateEffect == null) { yield break; } eyeStateEffect.ApplyUnfocusedEffect(target, this); yield return new WaitForSeconds(GetEffectTransitionDuration()); } private void OnValidate() { ApplyVisualSettingsNow( previewIdleState: !Application.isPlaying, replayFocusedEffect: Application.isPlaying && replayFocusedEffectOnSettingsChange, touchViewportOverlay: false); } /// /// 将 Visual Tuning 推送到所有 EyeTarget。Edit 模式预览 idle;Play 模式可选重播当前目标效果。 /// public void ApplyVisualSettingsNow( bool previewIdleState = false, bool replayFocusedEffect = false, bool touchViewportOverlay = true) { if (targets == null) { return; } foreach (EyeTarget target in targets) { if (target == null) { continue; } target.BindVisualSettings(visualSettings); if (previewIdleState) { target.ApplyIdlePreviewState(); } } if (touchViewportOverlay && Application.isPlaying) { EnsureViewportOverlay()?.ApplySettings(visualSettings); } if (!replayFocusedEffect || !Application.isPlaying || currentTarget == null || eyeStateEffect == null) { return; } currentTarget.KillVisualTweens(); eyeStateEffect.ApplyFocusedEffect(currentTarget, this); } private void ApplyVisualSettingsToTargets() { ApplyVisualSettingsNow(); } public enum EyeColorState { CanImagineColor, CannotImagineColor, EyeDisorder, CanSeeColor, HaveChip } private void Awake() { Init(); } private EyeViewportOverlay EnsureViewportOverlay() { if (viewportOverlay != null) { return viewportOverlay; } viewportOverlay = GetComponentInChildren(true); if (viewportOverlay == null) { viewportOverlay = gameObject.AddComponent(); } return viewportOverlay; } private bool EnsureMouseFollowAndZoom() { if (mouseFollowAndZoom != null) { return true; } mouseFollowAndZoom = FindObjectOfType(true); if (mouseFollowAndZoom == null) { Debug.LogError("[EyeSystem] MouseFollowAndZoom 未找到,无法进入视觉模块。", this); return false; } return true; } private Vector2 GetFocusViewportSize() { if (EnsureMouseFollowAndZoom()) { Vector2 viewportSize = mouseFollowAndZoom.GetViewportWorldSize(); if (viewportSize.sqrMagnitude > 0f) { return viewportSize; } } Camera focusCamera = CameraKit.Instance != null ? CameraKit.Instance.GetCurrentCamera() : Camera.main; if (focusCamera == null || !focusCamera.orthographic) { return Vector2.zero; } float height = focusCamera.orthographicSize * 2f; return new Vector2(height * focusCamera.aspect, height); } private static Transform GetFocusTransform(EyeTarget target) { if (target == null) { return null; } return target.targetTransform ? target.targetTransform : target.transform; } public void FocusOnTarget(EyeTarget target, float duration) { Transform focusTransform = GetFocusTransform(target); if (focusTransform == null || !EnsureMouseFollowAndZoom()) { return; } if (duration > 0.01f) { PlayPresentationSfx("event:/ActionFB/visualturn"); } mouseFollowAndZoom.MoveFocusTo( focusTransform, ScaleDuration(duration), focusMinBounds, focusMaxBounds, GetFocusViewportSize()); } public void Init() { FixSystemCenter.SystemDic.Register(this); } public EyeSnapshotDto CaptureEyeSnapshot() { return new EyeSnapshotDto { eyeColorState = _currentState.ToString() }; } public void ApplyEyeSnapshot(EyeSnapshotDto dto) { if (dto == null || string.IsNullOrEmpty(dto.eyeColorState)) { return; } if (Enum.TryParse(dto.eyeColorState, true, out EyeColorState state)) { SetEyeColorState(state); } } public void OnEnter() { if (!EnsureMouseFollowAndZoom()) { return; } mouseFollowAndZoom.SetIsActive(true); mouseFollowAndZoom.SetIsLock(false); mouseFollowAndZoom.ResetPanOrigin(); ApplyVisualSettingsToTargets(); InitializeAllTargets(); EnsureViewportOverlay()?.EnterModule(visualSettings); } /// /// UF 等密集演出:抑制非显式眨眼(含想象过渡眨眼);Yarn EyeBlink 仍可用。 /// public void SetNonExplicitBlinkSuppressed(bool suppressed) { EnsureViewportOverlay()?.SetNonExplicitBlinkSuppressed(suppressed); } private void InitializeAllTargets() { foreach (EyeTarget target in targets) { target.Init(); } if (currentTarget != null) { eyeStateEffect?.InitializeEffect(currentTarget, this); } } public void OnShow() { } public void OnExit() { EnsureViewportOverlay()?.ExitModule(); if (currentTarget != null) { eyeStateEffect?.CleanupEffect(CurrentTarget); } if (EnsureMouseFollowAndZoom()) { mouseFollowAndZoom.SetIsActive(false); } currentTarget = null; } private void Start() { ApplyVisualSettingsToTargets(); if (targets != null) { foreach (EyeTarget target in targets) { target?.Init(); } } SetEyeColorState(_currentState); EnsureMouseFollowAndZoom(); } public EyeTarget FindEyeTargetByName(string name) { return Targets.FirstOrDefault(target => target.gameObject.name == name); } public void SetTarget(EyeTarget newTarget) { if (newTarget == null) { Debug.LogError("Target is null in SetTarget"); return; } StartCoroutine(SetTargetCoroutine(newTarget.gameObject.name)); } [YarnCommand("EyeBlink")] public void EyeBlink(float duration = 0f) { EnsureViewportOverlay()?.PlayBlink( duration > 0f ? duration : null, restartAutoBlinkAfter: true, presentationSpeed: PresentationSpeed); } [YarnCommand("set_Eyetarget")] public IEnumerator SetTargetCoroutine(string targetName, bool startDialogue = true) { if (!EnsureMouseFollowAndZoom()) { yield break; } mouseFollowAndZoom.SetIsLock(true); EyeTarget target = FindEyeTargetByName(targetName); if (target == null) { Debug.LogError("Invalid EyeTarget name: " + targetName); yield break; } YarnVariableStorage.Instance.SetValue("$currentEyeTarget", target.name); if (currentTarget == target) { mouseFollowAndZoom.SetIndicatorTarget(currentTarget); if (startDialogue) { DialogController.Instance.StartDialogNode("重复看同一个目标"); yield return RunBlurOut(currentTarget, visualSettings.targetSwitchDuration); } yield break; } EyeTarget leavingTarget = currentTarget; if (leavingTarget != null) { ApplyUnfocusedEffectSilent(leavingTarget); } const float focusMoveDuration = 1f; float scaledFocusMove = ScaleDuration(focusMoveDuration); FocusOnTarget(target, focusMoveDuration); yield return WaitForFocusMovement(scaledFocusMove); currentTarget = target; mouseFollowAndZoom.SetIndicatorTarget(currentTarget); yield return RunBlurOut(currentTarget, visualSettings.targetSwitchDuration); if (startDialogue) { DialogController.Instance.StartDialogNode("IntoEyeView"); } } [YarnCommand("Set_MouseMovementLockState")] public void SetMouseMovementLockState(bool isActive) { if (!EnsureMouseFollowAndZoom()) { return; } mouseFollowAndZoom.SetIsLock(isActive); } [YarnCommand("EyeEffect_FadeIn")] public IEnumerator EyeEffect_FadeIn() { if (currentTarget == null) { yield break; } if (_currentState == EyeColorState.CanImagineColor && currentTarget.IsAtFocusedColorState(visualSettings)) { yield break; } PlayPresentationSfx("event:/ActionFB/color_imagine"); yield return RunFocusedEffectWithPreBlink(currentTarget); } [YarnCommand("EyeEffect_FadeOut")] public IEnumerator EyeEffect_FadeOut() { if (currentTarget == null) { yield break; } if (_currentState == EyeColorState.CanImagineColor && currentTarget.IsAtUnfocusedColorState(visualSettings)) { yield break; } PlayPresentationSfx("event:/ActionFB/color_imagine"); yield return RunUnfocusedEffect(currentTarget); } [YarnCommand("EyeEffect_Init")] public void EyeEffectInit() { ApplyVisualSettingsToTargets(); InitializeAllTargets(); } [YarnCommand("EyeEffect_Clean")] public void EyeEffectClean() { if (currentTarget != null) { eyeStateEffect?.CleanupEffect(currentTarget); } } [YarnCommand("set_EyeColorState")] public void SetEyeColorStateForYarn(string state) { if (!Enum.TryParse(state, true, out EyeColorState eyeState)) { throw new ArgumentException("Invalid eye state: " + state, nameof(state)); } SetEyeColorState(eyeState); } private void SetEyeColorState(EyeColorState state) { _currentState = state; eyeStateEffect = state switch { EyeColorState.CanImagineColor => new CanImagineColorEffect(), EyeColorState.CannotImagineColor => new CannotImagineColorEffect(), EyeColorState.EyeDisorder => new EyeDisorderEffect(), EyeColorState.CanSeeColor => new CanSeeColorEffect(), EyeColorState.HaveChip => new HaveChip(), _ => throw new ArgumentException("Unknown eye state: " + state, nameof(state)) }; } [YarnCommand("Set_Saturation")] public IEnumerator Set_Saturation(float targetSaturation, float duration) { targetSaturation = Mathf.Clamp01(targetSaturation); duration = ScaleDuration(duration); Sequence saturationSequence = DOTween.Sequence(); if (!TryJoinTargetSaturation(saturationSequence, targetSaturation, duration)) { Debug.LogWarning("[EyeSystem] 无可用 EyeTarget 饱和度 tween。", this); yield break; } Tween saturationTween = ScreenEffectManager.Instance != null ? ScreenEffectManager.Instance.TweenSaturation(targetSaturation, duration) : null; if (saturationTween != null) { saturationSequence.Join(saturationTween); } yield return saturationSequence.WaitForCompletion(); } [YarnCommand("EyeGlitch_in")] public IEnumerator EyeGlitch_in() { if (!TryStartUnifiedGlitch()) { Debug.LogWarning("[EyeSystem] 无可用 EyeTarget Glitch tween。", this); } yield return new WaitForSeconds(ScaleDuration(visualSettings.glitchInDuration)); } [YarnCommand("EyeGlitch_out")] public void EyeGlitch_out(float duration) { float resolved = duration > 0f ? duration : visualSettings.glitchOutDuration; TryStopUnifiedGlitch(ScaleDuration(resolved)); } [YarnCommand("set_EyeColor")] public IEnumerator SetEyeColor(string color, string memoryName = null) { if (memoryRender == null) { Debug.LogError("[EyeSystem] memoryRender 未赋值。", this); yield break; } Material memoryMaterial = memoryRender.material; memoryRender.sprite = null; memoryMaterial.SetFloat("_FullDistortionFade", 0f); memoryMaterial.SetFloat("_SqueezePower", 18f); if (memoryName != null) { var memoryKey = string.Format(MemorySpritePath, memoryName); var memoryHandle = ResourceSystem.LoadAsync(memoryKey); yield return memoryHandle; if (memoryHandle.Status != AsyncOperationStatus.Succeeded || memoryHandle.Result == null) { Debug.LogWarning("Memory not found: " + memoryKey); yield break; } memoryRender.sprite = memoryHandle.Result; memoryRender.gameObject.SetActive(true); float fadeIn = ScaleDuration(1.5f); float hold = ScaleDuration(1.5f); float squeeze = ScaleDuration(2f); float fadeOut = ScaleDuration(0.5f); float fadeOutAt = fadeIn + hold + squeeze; Sequence memorySequence = DOTween.Sequence(); memorySequence.Append(memoryMaterial.DOFloat(0.85f, "_FullDistortionFade", fadeIn)); memorySequence.AppendInterval(hold); memorySequence.Append(memoryMaterial.DOFloat(0f, "_SqueezePower", squeeze)); memorySequence.Insert(fadeOutAt, memoryMaterial.DOFloat(0f, "_FullDistortionFade", fadeOut).OnComplete(() => { memoryRender.gameObject.SetActive(false); })); yield return memorySequence.WaitForCompletion(); } Sequence sequence = DOTween.Sequence(); Sequence waveIn = CreateTargetRoundWaveSequence(1f, ScaleDuration(visualSettings.roundWaveInDuration)); if (waveIn != null) { sequence.Append(waveIn); } sequence.AppendCallback(() => { EyeGlitch_out(visualSettings.glitchOutDuration); }); Sequence waveOut = CreateTargetRoundWaveSequence(0f, ScaleDuration(visualSettings.roundWaveOutDuration)); if (waveOut != null) { sequence.Append(waveOut); } sequence.Play(); yield return sequence.WaitForCompletion(); } [YarnCommand("EyeGetColor")] public void GetColor() { SetEyeColorState(EyeColorState.CanImagineColor); foreach (EyeTarget target in targets) { target.ColorBack(); } } public void SetUnifiedUfWeights(float ascii, float dream, float memory) { foreach (EyeTarget target in targets) { target.SetUfWeights(ascii, dream, memory); } } public Tween TweenUnifiedUfWeights(float ascii, float dream, float memory, float duration) { duration = ScaleDuration(duration); Sequence sequence = DOTween.Sequence(); bool hasTween = false; foreach (EyeTarget target in targets) { Tween tween = target.TweenUfWeights(ascii, dream, memory, duration); if (tween == null) { continue; } sequence.Join(tween); hasTween = true; } return hasTween ? sequence : null; } private bool TryJoinTargetSaturation(Sequence sequence, float saturation, float duration) { bool hasTween = false; foreach (EyeTarget target in targets) { Tween tween = target.TweenSaturation(saturation, duration); if (tween == null) { continue; } sequence.Join(tween); hasTween = true; } return hasTween; } private bool TryStartUnifiedGlitch() { glitchin?.Kill(); glitchin = DOTween.Sequence(); bool hasTween = false; foreach (EyeTarget target in targets) { Sequence targetGlitch = target.StartUnifiedGlitch(ScaleDuration(visualSettings.glitchInDuration)); if (targetGlitch == null) { continue; } glitchin.Join(targetGlitch); hasTween = true; } return hasTween; } private bool TryStopUnifiedGlitch(float duration) { bool hasTarget = false; if (glitchin != null && glitchin.IsActive()) { glitchin.Kill(); hasTarget = true; } foreach (EyeTarget target in targets) { if (!target.TryGetUnifiedVisual(out _)) { continue; } target.StopUnifiedGlitch(duration); hasTarget = true; } return hasTarget; } private Sequence CreateTargetRoundWaveSequence(float value, float duration) { Sequence sequence = DOTween.Sequence(); bool hasTween = false; foreach (EyeTarget target in targets) { Tween tween = target.TweenRoundWave(value, duration); if (tween != null) { sequence.Join(tween); hasTween = true; } } return hasTween ? sequence : null; } } }