Files
aibis-dream/Assets/Scripts/MiniGame/Peipei/Eye/EyeSystem.cs
T

795 lines
25 KiB
C#

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<EyeTarget> targets = new();
public List<EyeTarget> 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;
/// <summary>Eye/UF 演出倍速(默认见 Visual Tuning → Presentation Speed)。</summary>
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);
}
/// <summary>仅「可想象色彩」上色时眨眼;无 autoBlink / 失焦眨眼。</summary>
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);
}
/// <summary>
/// 换目标时的失焦:只做效果,不眨眼(眨眼仅在想象上色 FadeIn)。
/// CanImagineColor 下若颜色已是失焦态,仍需补 BlurIn,否则未上色过的目标会一直保持清晰。
/// </summary>
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);
}
/// <summary>
/// 将 Visual Tuning 推送到所有 EyeTarget。Edit 模式预览 idle;Play 模式可选重播当前目标效果。
/// </summary>
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<EyeViewportOverlay>(true);
if (viewportOverlay == null)
{
viewportOverlay = gameObject.AddComponent<EyeViewportOverlay>();
}
return viewportOverlay;
}
private bool EnsureMouseFollowAndZoom()
{
if (mouseFollowAndZoom != null)
{
return true;
}
mouseFollowAndZoom = FindObjectOfType<MouseFollowAndZoom>(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);
}
/// <summary>
/// UF 等密集演出:抑制非显式眨眼(含想象过渡眨眼);Yarn EyeBlink 仍可用。
/// </summary>
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<Sprite>(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;
}
}
}