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, -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; public float GetEffectTransitionDuration() => visualSettings.GetDurationForState(_currentState); private bool ShouldPreBlinkForFocusedEffect() { return _currentState is EyeColorState.CanImagineColor or EyeColorState.CannotImagineColor or EyeColorState.CanSeeColor; } 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 && ShouldPreBlinkForFocusedEffect()) { if (_currentState == EyeColorState.CanImagineColor) { yield return overlay.PlayPreEffectBlinkThen( 2, visualSettings, () => eyeStateEffect?.ApplyFocusedEffect(target, this), visualSettings.imagineEffectAtBlinkOpen, visualSettings.imagineBlinkGap); } else { yield return overlay.PlayPreEffectBlinkThen( 1, visualSettings, () => eyeStateEffect?.ApplyFocusedEffect(target, this), visualSettings.focusEffectAtBlinkOpen); } } else { eyeStateEffect?.ApplyFocusedEffect(target, this); } yield return new WaitForSeconds(duration); } private IEnumerator RunBlurOutWithPreBlink(EyeTarget target, float duration) { EyeViewportOverlay overlay = EnsureViewportOverlay(); if (overlay != null && Application.isPlaying) { yield return overlay.PlayPreEffectBlinkThen( 1, visualSettings, () => target.BlurOut(duration), visualSettings.focusEffectAtBlinkOpen); } else { target.BlurOut(duration); } yield return new WaitForSeconds(duration); } private IEnumerator RunUnfocusedEffectWithPreBlink(EyeTarget target) { if (target == null || eyeStateEffect == null) { yield break; } float duration = GetEffectTransitionDuration(); EyeViewportOverlay overlay = EnsureViewportOverlay(); if (overlay != null && Application.isPlaying && ShouldPreBlinkForFocusedEffect()) { if (_currentState == EyeColorState.CanImagineColor) { yield return overlay.PlayPreEffectBlinkThen( 2, visualSettings, () => eyeStateEffect.ApplyUnfocusedEffect(target, this), visualSettings.imagineEffectAtBlinkOpen, visualSettings.imagineBlinkGap); } else { yield return overlay.PlayPreEffectBlinkThen( 1, visualSettings, () => eyeStateEffect.ApplyUnfocusedEffect(target, this), visualSettings.focusEffectAtBlinkOpen); } } else { eyeStateEffect.ApplyUnfocusedEffect(target, this); } yield return new WaitForSeconds(duration); } 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() { var focusCamera = 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) { AudioManager.Instance.PlaySfx("event:/ActionFB/visualturn"); } mouseFollowAndZoom.MoveFocusTo( focusTransform, 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); } 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); } [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 RunBlurOutWithPreBlink(currentTarget, visualSettings.targetSwitchDuration); } yield break; } EyeTarget leavingTarget = currentTarget; if (leavingTarget != null) { StartCoroutine(EyeEffect_FadeOut()); } const float focusMoveDuration = 1f; FocusOnTarget(target, focusMoveDuration); yield return WaitForFocusMovement(focusMoveDuration); currentTarget = target; mouseFollowAndZoom.SetIndicatorTarget(currentTarget); yield return RunBlurOutWithPreBlink(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; } AudioManager.Instance.PlaySfx("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; } AudioManager.Instance.PlaySfx("event:/ActionFB/color_imagine"); yield return RunUnfocusedEffectWithPreBlink(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); 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(visualSettings.glitchInDuration); } [YarnCommand("EyeGlitch_out")] public void EyeGlitch_out(float duration) { TryStopUnifiedGlitch(duration > 0f ? duration : visualSettings.glitchOutDuration); } [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); Sequence memorySequence = DOTween.Sequence(); memorySequence.Append(memoryMaterial.DOFloat(0.85f, "_FullDistortionFade", 1.5f)); memorySequence.AppendInterval(1.5f); memorySequence.Append(memoryMaterial.DOFloat(0f, "_SqueezePower", 2f)); memorySequence.Insert(4.5f, memoryMaterial.DOFloat(0f, "_FullDistortionFade", 0.5f).OnComplete(() => { memoryRender.gameObject.SetActive(false); })); yield return memorySequence.WaitForCompletion(); } Sequence sequence = DOTween.Sequence(); Sequence waveIn = CreateTargetRoundWaveSequence(1f, visualSettings.roundWaveInDuration); if (waveIn != null) { sequence.Append(waveIn); } sequence.AppendCallback(() => { EyeGlitch_out(visualSettings.glitchOutDuration); }); Sequence waveOut = CreateTargetRoundWaveSequence(0f, 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) { 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(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; } } }