1039 lines
38 KiB
C#
1039 lines
38 KiB
C#
using System.Collections;
|
||
using AibisDream.Kit;
|
||
using AibisDream.UI;
|
||
using AibisDream.Utility;
|
||
using DG.Tweening;
|
||
using UnityEngine;
|
||
|
||
namespace AibisDream
|
||
{
|
||
/// <summary>
|
||
/// Day2 门演出。可见门框与门后内容分层如下,勿混用:
|
||
/// <list type="bullet">
|
||
/// <item><b>可见门框(近景)</b>:只用 SpriteShowcase 大图(白色门框/金色门框)。
|
||
/// 缩放/透明度走 set_door_closeup / tween_door_closeup,不要再 show_large_sprite「门框特写」叠一层。</item>
|
||
/// <item><b>Door/background</b>:HDR 白底或门后内容,从大图透明洞透出。</item>
|
||
/// <item><b>Door/Frame</b>:场景里保持 inactive,缩放与近景大图不同;仅作 terminalShapeReference / 贴图同步,不要 SetActive 当第二层门框。</item>
|
||
/// <item><b>远景整门立绘</b>:hide_dream_door + show_large_sprite「D2S门」,不走本系统 Frame。</item>
|
||
/// </list>
|
||
/// </summary>
|
||
public class DreamDoorSystem : Singleton<DreamDoorSystem>
|
||
{
|
||
[Header("Door Root")]
|
||
[Tooltip("Door 根节点。show_door / listen_view 时激活;内含 background、海浪等,不含「可见近景门框」。")]
|
||
[SerializeField] private Transform doorRoot;
|
||
|
||
[Header("Door Sprites")]
|
||
[SerializeField] private SpriteRenderer glassSea;
|
||
[SerializeField] private SpriteRenderer codeSea;
|
||
[SerializeField] private SpriteRenderer flashOverlay;
|
||
[Tooltip("门后 HDR 白底等。不要把 glassSea/codeSea/overlay 拖进来。")]
|
||
[SerializeField] private Renderer[] backgroundRenderers;
|
||
[Tooltip("场景 Frame:默认 inactive,勿当作近景可见门框。可与 terminalShapeReference 共用。")]
|
||
[SerializeField] private Renderer[] frameRenderers;
|
||
[SerializeField] private Sprite whiteDoorFrameSprite;
|
||
[SerializeField] private Sprite seaDoorFrameSprite;
|
||
[SerializeField] private SpriteRenderer sceneBlackOverlay;
|
||
|
||
[Header("Door Sea Animation")]
|
||
[SerializeField] private Animator glassSeaAnimator;
|
||
[SerializeField] private string glassSeaStateName = "DoorGlassSeaLoop";
|
||
[SerializeField] private string glassSeaFramedStateName = "DoorGlassSeaFramedLoop";
|
||
[SerializeField, Min(1f)] private float glassSeaCoverOverscan = 1.02f;
|
||
|
||
[Header("Code Sea Animation")]
|
||
[Tooltip("代码海切入时的固定波浪相位(单位:字符网格行)。用于匹配金色海浪切换前的构图。")]
|
||
[SerializeField] private float codeSeaStartPhase = 7.5f;
|
||
[Tooltip("代码海由远向近推进的速度(单位:字符网格行/秒)。")]
|
||
[SerializeField, Min(0f)] private float codeSeaWaveSpeed = 2.6f;
|
||
[Tooltip("0/1 和波浪符号独立于海浪移动的闪烁速度。")]
|
||
[SerializeField, Min(0f)] private float codeSeaGlyphSpeed = 3.2f;
|
||
|
||
[Header("Terminal(挂在 Door 子节点上即可,无需 Canvas)")]
|
||
[SerializeField] private DreamTerminalUI terminalUI;
|
||
|
||
[Header("Morph Settings")]
|
||
[Tooltip("负责门壳贴合的节点;不填则退回到 doorRoot。")]
|
||
[SerializeField] private Transform doorShellTransform;
|
||
[Tooltip("线框先贴合的图片目标。建议拖一个 SpriteRenderer 作为参考形状。")]
|
||
[SerializeField] private SpriteRenderer terminalShapeReference;
|
||
[SerializeField] private Vector3 seaCollapseScale = new(0.9f, 0.84f, 1f);
|
||
[SerializeField] private Vector3 doorSnapScale = new(0.95f, 1.06f, 1f);
|
||
[SerializeField] private Ease morphEase = Ease.OutBack;
|
||
[SerializeField] private FrameHidePhase frameHidePhase = FrameHidePhase.OnTerminal;
|
||
[SerializeField] private float doorSnapDuration = 0.2f;
|
||
[SerializeField] private float backgroundCutDelay = 0.04f;
|
||
[Tooltip("变形前停顿,稍长一点更有「蓄力→啪」的顿挫。")]
|
||
[SerializeField] private float preMorphHoldDuration = 0.22f;
|
||
[SerializeField] private float postMorphBootDelay = 0.06f;
|
||
[SerializeField] private float referenceSnapMorphProgress = 0.08f;
|
||
[SerializeField] private Vector2 referenceSnapPadding = new(0.18f, 0.18f);
|
||
[Tooltip("终端变形时 sceneBlackOverlay 淡入目标透明度(背景压暗)")]
|
||
[SerializeField] private float blackoutAlpha = 0.92f;
|
||
[Tooltip("黑场淡入时长(与终端 morph 并行)")]
|
||
[SerializeField] private float blackoutFadeInDuration = 0.08f;
|
||
[SerializeField] private float blackoutFadeOutDuration = 0.12f;
|
||
[Tooltip("变形为终端开始时并行播放的 Timeline(TimelineCenter 名称,如 姐姐/头痛;留空则不播)")]
|
||
[SerializeField] private string morphStartTimelineName = "姐姐/Glitch";
|
||
[Tooltip("门框→终端变形时长。偏短配合 OutBack 更容易做出顿挫。")]
|
||
[SerializeField, Min(0.05f)] private float morphDuration = 0.45f;
|
||
|
||
[Header("Ambient")]
|
||
[Tooltip("变门/终端时立刻关掉的场景背景层(如星空)。须在 HideLargeFrame 之前关掉,否则大图一收会穿帮。")]
|
||
[SerializeField] private GameObject[] hideOnTerminal;
|
||
|
||
private Transform _codeSeaTransform;
|
||
private Vector3 _codeSeaOriginalLocalPosition;
|
||
private Vector3 _codeSeaOriginalScale;
|
||
private Color _codeSeaOriginalColor;
|
||
private Transform _terminalUiTransform;
|
||
private Vector3 _terminalUiOriginalScale;
|
||
private Vector3 _terminalUiOriginalLocalPosition;
|
||
private Transform _doorShellTransform;
|
||
private Vector3 _doorShellOriginalScale;
|
||
private Vector3 _glassSeaOriginalLocalPosition;
|
||
private Vector3 _glassSeaOriginalLocalScale;
|
||
private string _glassSeaCurrentStateName;
|
||
private MaterialPropertyBlock _codeSeaPropertyBlock;
|
||
private bool _codeSeaPlaybackActive;
|
||
private Coroutine _closeupCoroutine;
|
||
private static readonly int WaveStartTimeId = Shader.PropertyToID("_WaveStartTime");
|
||
private static readonly int WavePhaseId = Shader.PropertyToID("_WavePhase");
|
||
private static readonly int WaveSpeedId = Shader.PropertyToID("_WaveSpeed");
|
||
private static readonly int GlyphSpeedId = Shader.PropertyToID("_GlyphSpeed");
|
||
private DoorPresentationPhase _presentationPhase = DoorPresentationPhase.Hidden;
|
||
private enum FrameHidePhase
|
||
{
|
||
Never,
|
||
OnCodeSea,
|
||
OnTerminal
|
||
}
|
||
|
||
private enum DoorPresentationPhase
|
||
{
|
||
Hidden,
|
||
Door,
|
||
GlassSea,
|
||
CodeSea,
|
||
Terminal
|
||
}
|
||
|
||
public override void OnSingletonInit()
|
||
{
|
||
if (doorRoot != null)
|
||
doorRoot.gameObject.SetActive(false);
|
||
|
||
_doorShellTransform = doorShellTransform != null ? doorShellTransform : doorRoot;
|
||
if (_doorShellTransform != null)
|
||
_doorShellOriginalScale = _doorShellTransform.localScale;
|
||
|
||
if (glassSea != null)
|
||
{
|
||
_glassSeaOriginalLocalPosition = glassSea.transform.localPosition;
|
||
_glassSeaOriginalLocalScale = glassSea.transform.localScale;
|
||
if (glassSeaAnimator == null)
|
||
glassSeaAnimator = glassSea.GetComponent<Animator>();
|
||
|
||
glassSea.SetAlpha(0);
|
||
StopGlassSeaAnimation(resetToFirstFrame: true);
|
||
glassSea.gameObject.SetActive(false);
|
||
}
|
||
|
||
SyncSceneFrameSprite(hasSea: false);
|
||
KeepSceneFrameInactive();
|
||
|
||
if (codeSea != null)
|
||
{
|
||
_codeSeaTransform = codeSea.transform;
|
||
_codeSeaOriginalLocalPosition = _codeSeaTransform.localPosition;
|
||
_codeSeaOriginalScale = _codeSeaTransform.localScale;
|
||
_codeSeaOriginalColor = codeSea.color;
|
||
codeSea.SetAlpha(0);
|
||
codeSea.gameObject.SetActive(false);
|
||
}
|
||
|
||
if (flashOverlay != null)
|
||
{
|
||
flashOverlay.SetAlpha(0);
|
||
flashOverlay.gameObject.SetActive(true);
|
||
}
|
||
|
||
if (sceneBlackOverlay != null)
|
||
{
|
||
sceneBlackOverlay.SetAlpha(0);
|
||
sceneBlackOverlay.gameObject.SetActive(true);
|
||
}
|
||
|
||
if (terminalUI != null)
|
||
{
|
||
terminalUI.ResetVisualState();
|
||
terminalUI.Alpha = 0f;
|
||
_terminalUiTransform = terminalUI.transform;
|
||
_terminalUiOriginalScale = _terminalUiTransform.localScale;
|
||
_terminalUiOriginalLocalPosition = _terminalUiTransform.localPosition;
|
||
}
|
||
|
||
_presentationPhase = DoorPresentationPhase.Hidden;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示普通门。该命令是一个完整状态切换,会清掉之前残留的海景、代码海浪和终端。
|
||
/// </summary>
|
||
public IEnumerator ShowDoor(float duration = 0f)
|
||
{
|
||
StopAllEffects();
|
||
EnsureDoorBaseVisible(hasSea: false);
|
||
ResetGlassSeaImmediate();
|
||
ResetCodeSeaImmediate();
|
||
ResetTerminalImmediate();
|
||
_presentationPhase = DoorPresentationPhase.Door;
|
||
|
||
if (duration > 0f)
|
||
yield return new WaitForSeconds(duration);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 门近景:大图显示白/金色门框 + Door 内 background/海浪。
|
||
/// Yarn 无需再先 show_large_sprite「门框特写」(会被本命令覆盖)。
|
||
/// </summary>
|
||
public IEnumerator ShowListenView(bool showSea, float duration = 0f)
|
||
{
|
||
StopAllEffects();
|
||
EnsureDoorBaseVisible(showSea);
|
||
ResetCodeSeaImmediate();
|
||
ResetTerminalImmediate();
|
||
ShowLargeFrame(hasSea: showSea, scale: 1f, alpha: 1f);
|
||
|
||
if (showSea && glassSea != null)
|
||
{
|
||
glassSea.SetAlpha(1f);
|
||
StartGlassSeaAnimation(glassSeaFramedStateName);
|
||
_presentationPhase = DoorPresentationPhase.GlassSea;
|
||
}
|
||
else
|
||
{
|
||
ResetGlassSeaImmediate();
|
||
_presentationPhase = DoorPresentationPhase.Door;
|
||
}
|
||
|
||
if (duration > 0f)
|
||
yield return new WaitForSeconds(duration);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 隐藏整个门
|
||
/// </summary>
|
||
public void HideDoor()
|
||
{
|
||
if (doorRoot != null)
|
||
doorRoot.gameObject.SetActive(false);
|
||
}
|
||
|
||
#region Phase 1: GlassSea 浮现
|
||
|
||
public IEnumerator ShowGlassSea(float duration = 2f)
|
||
{
|
||
if (glassSea == null)
|
||
yield break;
|
||
|
||
StopAllEffects();
|
||
EnsureDoorBaseVisible(hasSea: true);
|
||
ResetCodeSeaImmediate();
|
||
ResetTerminalImmediate();
|
||
|
||
if (_presentationPhase != DoorPresentationPhase.GlassSea || !glassSea.gameObject.activeSelf)
|
||
glassSea.SetAlpha(0f);
|
||
|
||
StartGlassSeaAnimation(glassSeaFramedStateName);
|
||
_presentationPhase = DoorPresentationPhase.GlassSea;
|
||
|
||
yield return glassSea.DOFade(1f, Mathf.Max(0f, duration))
|
||
.SetEase(Ease.InOutSine)
|
||
.WaitForCompletion();
|
||
}
|
||
|
||
public IEnumerator HideGlassSea(float duration = 1f)
|
||
{
|
||
if (glassSea == null)
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
DOTween.Kill(glassSea);
|
||
|
||
if (glassSea.gameObject.activeSelf)
|
||
{
|
||
yield return glassSea.DOFade(0f, Mathf.Max(0f, duration))
|
||
.SetEase(Ease.InOutSine)
|
||
.WaitForCompletion();
|
||
}
|
||
|
||
glassSea.SetAlpha(0f);
|
||
StopGlassSeaAnimation(resetToFirstFrame: true);
|
||
glassSea.gameObject.SetActive(false);
|
||
|
||
if (_presentationPhase == DoorPresentationPhase.GlassSea)
|
||
_presentationPhase = DoorPresentationPhase.Door;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 只调整近景门图片的缩放与透明度,不包含白幕、Bloom 或海浪状态切换。
|
||
/// </summary>
|
||
public IEnumerator TweenListenCloseup(float scale, float alpha, float duration)
|
||
{
|
||
StopCloseupCoroutine();
|
||
var presentation = Day2SleepPresentationController.Instance;
|
||
if (presentation == null)
|
||
yield break;
|
||
|
||
yield return presentation.AnimateLargePresentation(scale, alpha, duration);
|
||
}
|
||
|
||
/// <summary>异步近景;再次调用 set/tween/show_framed 时会取消上一次。</summary>
|
||
public void TweenListenCloseupAsync(float scale, float alpha, float duration)
|
||
{
|
||
StopCloseupCoroutine();
|
||
if (Day2SleepPresentationController.Instance == null)
|
||
return;
|
||
|
||
_closeupCoroutine = StartCoroutine(TweenListenCloseupRoutine(scale, alpha, duration));
|
||
}
|
||
|
||
public void SetListenCloseup(float scale, float alpha)
|
||
{
|
||
StopCloseupCoroutine();
|
||
Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(scale, alpha);
|
||
}
|
||
|
||
private IEnumerator TweenListenCloseupRoutine(float scale, float alpha, float duration)
|
||
{
|
||
var presentation = Day2SleepPresentationController.Instance;
|
||
if (presentation == null)
|
||
yield break;
|
||
yield return presentation.AnimateLargePresentation(scale, alpha, duration);
|
||
_closeupCoroutine = null;
|
||
}
|
||
|
||
private void StopCloseupCoroutine()
|
||
{
|
||
if (_closeupCoroutine != null)
|
||
{
|
||
StopCoroutine(_closeupCoroutine);
|
||
_closeupCoroutine = null;
|
||
}
|
||
Day2SleepPresentationController.Instance?.StopLargePresentationAnimation();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 全白时:隐藏门底与大图门框,仅全屏金色海浪。
|
||
/// </summary>
|
||
public void ShowGlassSeaFullscreen()
|
||
{
|
||
if (doorRoot != null)
|
||
doorRoot.gameObject.SetActive(true);
|
||
|
||
SetBackgroundVisible(false);
|
||
HideLargeFrame();
|
||
ResetCodeSeaImmediate();
|
||
EnsureGlassSeaVisible(glassSeaStateName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 全白时:恢复大图门框,金色海浪留在门后。
|
||
/// </summary>
|
||
public void ShowGlassSeaFramed()
|
||
{
|
||
StopCloseupCoroutine();
|
||
ResetCodeSeaImmediate();
|
||
EnsureDoorBaseVisible(hasSea: true);
|
||
ShowLargeFrame(hasSea: true, scale: 1f, alpha: 1f);
|
||
EnsureGlassSeaVisible(glassSeaFramedStateName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 全白时:隐藏门底与大图门框,全屏 CodeSea。
|
||
/// </summary>
|
||
public void ShowCodeSeaFullscreen()
|
||
{
|
||
if (doorRoot != null)
|
||
doorRoot.gameObject.SetActive(true);
|
||
|
||
var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea
|
||
|| !_codeSeaPlaybackActive;
|
||
SetBackgroundVisible(false);
|
||
HideLargeFrame();
|
||
ResetGlassSeaImmediate();
|
||
EnsureCodeSeaVisible(restartPlayback);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 全白时:恢复大图白色门框(scale=1),CodeSea 留在门内。
|
||
/// 会取消进行中的 tween_door_closeup_async,避免停在 4x 放大。
|
||
/// </summary>
|
||
public void ShowCodeSeaFramed()
|
||
{
|
||
var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea
|
||
|| !_codeSeaPlaybackActive;
|
||
StopCloseupCoroutine();
|
||
ResetGlassSeaImmediate();
|
||
EnsureDoorBaseVisible(hasSea: false);
|
||
ShowLargeFrame(hasSea: false, scale: 1f, alpha: 1f);
|
||
EnsureCodeSeaFramedVisible(restartPlayback);
|
||
}
|
||
|
||
private void ResetListenTransitionImmediate()
|
||
{
|
||
var panel = UIManager.Instance != null
|
||
? UIManager.Instance.GetPanel<ScreenTransitionPanel>()
|
||
: null;
|
||
panel?.FadeOutSync(0f);
|
||
|
||
Day2SleepPresentationController.Instance?.ResetLargePresentationImmediate();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Phase 2: CodeSea 浮现 (GlassSea 交叉淡出)
|
||
|
||
public IEnumerator ShowCodeSea(float duration = 2f)
|
||
{
|
||
if (codeSea == null)
|
||
yield break;
|
||
|
||
var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea
|
||
|| !_codeSeaPlaybackActive;
|
||
StopAllEffects();
|
||
EnsureDoorBaseVisible(hasSea: false);
|
||
ResetTerminalImmediate();
|
||
|
||
codeSea.color = _codeSeaOriginalColor;
|
||
if (_codeSeaTransform != null)
|
||
{
|
||
_codeSeaTransform.localPosition = _codeSeaOriginalLocalPosition;
|
||
_codeSeaTransform.localScale = _codeSeaOriginalScale;
|
||
}
|
||
codeSea.SetAlpha(0f);
|
||
codeSea.gameObject.SetActive(true);
|
||
BeginCodeSeaPlayback(restartPlayback);
|
||
_presentationPhase = DoorPresentationPhase.CodeSea;
|
||
|
||
if (frameHidePhase == FrameHidePhase.OnCodeSea)
|
||
HideLargeFrame();
|
||
|
||
var seq = DOTween.Sequence();
|
||
seq.Append(codeSea.DOFade(1f, Mathf.Max(0f, duration)).SetEase(Ease.InOutSine));
|
||
if (glassSea != null && glassSea.gameObject.activeSelf)
|
||
{
|
||
seq.Join(glassSea.DOFade(0f, Mathf.Max(0f, duration) * 0.7f).SetEase(Ease.InSine));
|
||
}
|
||
|
||
yield return seq.WaitForCompletion();
|
||
|
||
if (glassSea != null)
|
||
{
|
||
glassSea.SetAlpha(0f);
|
||
StopGlassSeaAnimation(resetToFirstFrame: true);
|
||
glassSea.gameObject.SetActive(false);
|
||
}
|
||
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Phase 3: CodeSea 变形为终端
|
||
|
||
public IEnumerator ShowTerminal()
|
||
{
|
||
if (doorRoot != null)
|
||
doorRoot.gameObject.SetActive(true);
|
||
|
||
if (terminalUI != null)
|
||
terminalUI.gameObject.SetActive(true);
|
||
|
||
_presentationPhase = DoorPresentationPhase.Terminal;
|
||
yield return AnimateDoorToTerminal();
|
||
if (terminalUI != null)
|
||
{
|
||
if (postMorphBootDelay > 0f)
|
||
yield return new WaitForSeconds(postMorphBootDelay);
|
||
terminalUI.Begin();
|
||
}
|
||
}
|
||
|
||
private IEnumerator AnimateDoorToTerminal()
|
||
{
|
||
StopAllEffects();
|
||
|
||
// 先关星空等远景层,再收大图;否则 D2S门 一隐星空会露出来穿帮。
|
||
HideAmbientForTerminal();
|
||
|
||
if (terminalUI != null)
|
||
{
|
||
terminalUI.ResetVisualState();
|
||
terminalUI.MorphProgress = 0f;
|
||
terminalUI.Alpha = 1f;
|
||
}
|
||
|
||
if (_terminalUiTransform != null)
|
||
{
|
||
_terminalUiTransform.localPosition = _terminalUiOriginalLocalPosition;
|
||
_terminalUiTransform.localScale = _terminalUiOriginalScale;
|
||
}
|
||
|
||
SetBackgroundVisible(false);
|
||
HideLargeFrame();
|
||
|
||
if (glassSea != null)
|
||
{
|
||
StopGlassSeaAnimation(resetToFirstFrame: true);
|
||
glassSea.gameObject.SetActive(false);
|
||
}
|
||
|
||
if (codeSea != null)
|
||
{
|
||
codeSea.gameObject.SetActive(false);
|
||
ResetCodeSeaPlayback();
|
||
}
|
||
|
||
if (flashOverlay != null)
|
||
flashOverlay.SetAlpha(0f);
|
||
|
||
if (sceneBlackOverlay != null)
|
||
{
|
||
sceneBlackOverlay.gameObject.SetActive(true);
|
||
sceneBlackOverlay.SetAlpha(0f);
|
||
}
|
||
|
||
if (backgroundCutDelay > 0f)
|
||
yield return new WaitForSeconds(backgroundCutDelay);
|
||
|
||
if (preMorphHoldDuration > 0f)
|
||
yield return new WaitForSeconds(preMorphHoldDuration);
|
||
|
||
if (terminalUI != null)
|
||
{
|
||
float morph = 0f;
|
||
// OutBack 带轻微过冲,配合变短的 morphDuration,落地更有顿挫。
|
||
var morphTween = DOTween.To(() => morph, v =>
|
||
{
|
||
morph = v;
|
||
terminalUI.MorphProgress = v;
|
||
}, 1f, Mathf.Max(0.05f, morphDuration)).SetEase(morphEase);
|
||
|
||
if (!string.IsNullOrEmpty(morphStartTimelineName))
|
||
TimelineCenter.Instance.PlayTimeline(morphStartTimelineName);
|
||
|
||
if (sceneBlackOverlay != null)
|
||
sceneBlackOverlay.DOFade(blackoutAlpha, Mathf.Max(0.01f, blackoutFadeInDuration));
|
||
|
||
yield return morphTween.WaitForCompletion();
|
||
terminalUI.MorphProgress = 1f;
|
||
}
|
||
|
||
ResetDoorShellScale();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 终端结局动效
|
||
|
||
public IEnumerator PlayShutdownEffect()
|
||
{
|
||
var target = _terminalUiTransform != null ? _terminalUiTransform : _codeSeaTransform;
|
||
if (target == null) yield break;
|
||
|
||
var seq = DOTween.Sequence();
|
||
|
||
if (flashOverlay != null)
|
||
seq.Append(flashOverlay.DOFade(0.6f, 0.15f));
|
||
|
||
// 压扁成白线
|
||
float scaleY = _terminalUiTransform != null ? _terminalUiOriginalScale.y * 0.02f : 0.005f;
|
||
seq.Join(target.DOScaleY(scaleY, 0.35f).SetEase(Ease.OutQuad));
|
||
// 横向收缩消失
|
||
seq.Append(target.DOScaleX(0f, 0.3f).SetEase(Ease.InQuad));
|
||
|
||
if (flashOverlay != null)
|
||
seq.Join(flashOverlay.DOFade(0f, 0.5f));
|
||
|
||
yield return seq.WaitForCompletion();
|
||
}
|
||
|
||
public void PlayCrashEffect()
|
||
{
|
||
var target = _terminalUiTransform != null ? _terminalUiTransform : _codeSeaTransform;
|
||
if (target != null)
|
||
target.DOShakePosition(1.2f, 0.08f, 35, fadeOut: false);
|
||
}
|
||
|
||
public void StopAllEffects()
|
||
{
|
||
StopCloseupCoroutine();
|
||
DOTween.Kill(glassSea);
|
||
DOTween.Kill(_codeSeaTransform);
|
||
DOTween.Kill(codeSea);
|
||
DOTween.Kill(_terminalUiTransform);
|
||
DOTween.Kill(_doorShellTransform);
|
||
DOTween.Kill(flashOverlay);
|
||
DOTween.Kill(sceneBlackOverlay);
|
||
Day2SleepPresentationController.Instance?.StopLargePresentationAnimation();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 清理
|
||
|
||
public void HideAll()
|
||
{
|
||
StopAllEffects();
|
||
ResetListenTransitionImmediate();
|
||
HideDoor();
|
||
SetBackgroundVisible(true);
|
||
SyncSceneFrameSprite(hasSea: false);
|
||
KeepSceneFrameInactive();
|
||
ResetDoorShellScale();
|
||
|
||
if (glassSea != null)
|
||
{
|
||
glassSea.SetAlpha(0);
|
||
StopGlassSeaAnimation(resetToFirstFrame: true);
|
||
glassSea.gameObject.SetActive(false);
|
||
}
|
||
|
||
if (codeSea != null)
|
||
{
|
||
codeSea.color = _codeSeaOriginalColor;
|
||
codeSea.SetAlpha(0);
|
||
codeSea.gameObject.SetActive(false);
|
||
if (_codeSeaTransform != null)
|
||
_codeSeaTransform.localScale = _codeSeaOriginalScale;
|
||
}
|
||
ResetCodeSeaPlayback();
|
||
|
||
if (terminalUI != null)
|
||
{
|
||
terminalUI.Alpha = 0f;
|
||
terminalUI.ResetVisualState();
|
||
}
|
||
|
||
if (_terminalUiTransform != null)
|
||
{
|
||
_terminalUiTransform.localPosition = _terminalUiOriginalLocalPosition;
|
||
_terminalUiTransform.localScale = _terminalUiOriginalScale;
|
||
}
|
||
|
||
if (flashOverlay != null)
|
||
flashOverlay.SetAlpha(0);
|
||
|
||
if (sceneBlackOverlay != null)
|
||
sceneBlackOverlay.SetAlpha(0);
|
||
|
||
HideLargeFrame();
|
||
|
||
_presentationPhase = DoorPresentationPhase.Hidden;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Presentation State
|
||
|
||
/// <summary>
|
||
/// 打开 Door 根与门后背景;不同时显示场景 Frame(近景门框只走大图)。
|
||
/// </summary>
|
||
private void EnsureDoorBaseVisible(bool hasSea = false)
|
||
{
|
||
if (doorRoot != null)
|
||
doorRoot.gameObject.SetActive(true);
|
||
|
||
// 梦境「遮罩」是纯黑全屏;不关会从门框透明洞透出黑底,盖住 HDR 白光。
|
||
SpriteShowcase.Instance?.HideBackgroundImmediate();
|
||
SetBackgroundVisible(true);
|
||
KeepSceneFrameInactive();
|
||
SyncSceneFrameSprite(hasSea);
|
||
ResetDoorShellScale();
|
||
}
|
||
|
||
/// <summary>可见门框:大图贴白/金色门框并设缩放透明度。</summary>
|
||
private void ShowLargeFrame(bool hasSea, float scale, float alpha)
|
||
{
|
||
var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite;
|
||
SyncSceneFrameSprite(hasSea);
|
||
var presentation = Day2SleepPresentationController.Instance;
|
||
if (presentation == null) return;
|
||
if (targetSprite != null)
|
||
presentation.SetLargeSpriteImmediate(targetSprite);
|
||
presentation.SetLargePresentationImmediate(scale, alpha);
|
||
}
|
||
|
||
private void HideLargeFrame()
|
||
{
|
||
Day2SleepPresentationController.Instance?.HideLargePresentationImmediate();
|
||
}
|
||
|
||
/// <summary>只同步场景 Frame 贴图,不激活(供 morph 参考一致)。</summary>
|
||
private void SyncSceneFrameSprite(bool hasSea)
|
||
{
|
||
var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite;
|
||
if (targetSprite == null || frameRenderers == null) return;
|
||
|
||
foreach (var frameRenderer in frameRenderers)
|
||
{
|
||
if (frameRenderer is SpriteRenderer spriteRenderer)
|
||
spriteRenderer.sprite = targetSprite;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 场景 Frame 必须保持 inactive。其 localScale≈0.73,若被激活会叠出一层「缩小门框」。
|
||
/// </summary>
|
||
private void KeepSceneFrameInactive()
|
||
{
|
||
if (frameRenderers == null) return;
|
||
foreach (var frameRenderer in frameRenderers)
|
||
{
|
||
if (frameRenderer == null) continue;
|
||
if (frameRenderer.gameObject.activeSelf)
|
||
frameRenderer.gameObject.SetActive(false);
|
||
frameRenderer.enabled = true;
|
||
}
|
||
}
|
||
|
||
private void HideAmbientForTerminal()
|
||
{
|
||
if (hideOnTerminal == null) return;
|
||
foreach (var go in hideOnTerminal)
|
||
{
|
||
if (go != null && go.activeSelf)
|
||
go.SetActive(false);
|
||
}
|
||
}
|
||
|
||
private void ResetGlassSeaImmediate()
|
||
{
|
||
DOTween.Kill(glassSea);
|
||
|
||
if (glassSea != null)
|
||
{
|
||
glassSea.SetAlpha(0f);
|
||
StopGlassSeaAnimation(resetToFirstFrame: true);
|
||
glassSea.gameObject.SetActive(false);
|
||
}
|
||
}
|
||
|
||
private void EnsureGlassSeaVisible(string stateName)
|
||
{
|
||
if (glassSea == null)
|
||
return;
|
||
|
||
glassSea.SetAlpha(1f);
|
||
if (!glassSea.gameObject.activeSelf
|
||
|| glassSeaAnimator == null
|
||
|| !glassSeaAnimator.enabled
|
||
|| _glassSeaCurrentStateName != stateName)
|
||
StartGlassSeaAnimation(stateName);
|
||
else
|
||
FitGlassSeaToCameraCover();
|
||
|
||
_presentationPhase = DoorPresentationPhase.GlassSea;
|
||
}
|
||
|
||
private void ResetCodeSeaImmediate()
|
||
{
|
||
DOTween.Kill(_codeSeaTransform);
|
||
DOTween.Kill(codeSea);
|
||
|
||
ResetCodeSeaPlayback();
|
||
|
||
if (codeSea == null)
|
||
return;
|
||
|
||
codeSea.color = _codeSeaOriginalColor;
|
||
codeSea.SetAlpha(0f);
|
||
codeSea.gameObject.SetActive(false);
|
||
|
||
if (_codeSeaTransform != null)
|
||
{
|
||
_codeSeaTransform.localPosition = _codeSeaOriginalLocalPosition;
|
||
_codeSeaTransform.localScale = _codeSeaOriginalScale;
|
||
}
|
||
}
|
||
|
||
private void EnsureCodeSeaVisible(bool restartPlayback)
|
||
{
|
||
if (codeSea == null)
|
||
return;
|
||
|
||
codeSea.color = _codeSeaOriginalColor;
|
||
codeSea.SetAlpha(1f);
|
||
codeSea.gameObject.SetActive(true);
|
||
BeginCodeSeaPlayback(restartPlayback);
|
||
FitCodeSeaToCameraCover();
|
||
_presentationPhase = DoorPresentationPhase.CodeSea;
|
||
}
|
||
|
||
private void EnsureCodeSeaFramedVisible(bool restartPlayback)
|
||
{
|
||
if (codeSea == null)
|
||
return;
|
||
|
||
codeSea.color = _codeSeaOriginalColor;
|
||
codeSea.SetAlpha(1f);
|
||
codeSea.gameObject.SetActive(true);
|
||
BeginCodeSeaPlayback(restartPlayback);
|
||
|
||
if (_codeSeaTransform != null)
|
||
{
|
||
_codeSeaTransform.localPosition = _codeSeaOriginalLocalPosition;
|
||
_codeSeaTransform.localScale = _codeSeaOriginalScale;
|
||
}
|
||
|
||
_presentationPhase = DoorPresentationPhase.CodeSea;
|
||
}
|
||
|
||
private void BeginCodeSeaPlayback(bool restartPlayback)
|
||
{
|
||
if (codeSea == null || (_codeSeaPlaybackActive && !restartPlayback))
|
||
return;
|
||
|
||
_codeSeaPropertyBlock ??= new MaterialPropertyBlock();
|
||
codeSea.GetPropertyBlock(_codeSeaPropertyBlock);
|
||
_codeSeaPropertyBlock.SetFloat(WaveStartTimeId, Time.time);
|
||
_codeSeaPropertyBlock.SetFloat(WavePhaseId, codeSeaStartPhase);
|
||
_codeSeaPropertyBlock.SetFloat(WaveSpeedId, codeSeaWaveSpeed);
|
||
_codeSeaPropertyBlock.SetFloat(GlyphSpeedId, codeSeaGlyphSpeed);
|
||
codeSea.SetPropertyBlock(_codeSeaPropertyBlock);
|
||
_codeSeaPlaybackActive = true;
|
||
}
|
||
|
||
private void ResetCodeSeaPlayback()
|
||
{
|
||
_codeSeaPlaybackActive = false;
|
||
}
|
||
|
||
private void ResetTerminalImmediate()
|
||
{
|
||
DOTween.Kill(_terminalUiTransform);
|
||
|
||
if (terminalUI != null)
|
||
{
|
||
terminalUI.Alpha = 0f;
|
||
terminalUI.ResetVisualState();
|
||
}
|
||
|
||
if (_terminalUiTransform != null)
|
||
{
|
||
_terminalUiTransform.localPosition = _terminalUiOriginalLocalPosition;
|
||
_terminalUiTransform.localScale = _terminalUiOriginalScale;
|
||
}
|
||
|
||
if (flashOverlay != null)
|
||
flashOverlay.SetAlpha(0f);
|
||
|
||
if (sceneBlackOverlay != null)
|
||
sceneBlackOverlay.SetAlpha(0f);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region GlassSea Animation
|
||
|
||
private void StartGlassSeaAnimation(string stateName = null)
|
||
{
|
||
if (glassSea == null) return;
|
||
|
||
stateName = string.IsNullOrEmpty(stateName) ? glassSeaStateName : stateName;
|
||
|
||
glassSea.gameObject.SetActive(true);
|
||
if (glassSeaAnimator != null && glassSeaAnimator.runtimeAnimatorController != null)
|
||
{
|
||
glassSeaAnimator.enabled = true;
|
||
glassSeaAnimator.Play(stateName, 0, 0f);
|
||
glassSeaAnimator.Update(0f);
|
||
_glassSeaCurrentStateName = stateName;
|
||
}
|
||
|
||
FitGlassSeaToCameraCover();
|
||
}
|
||
|
||
private void StopGlassSeaAnimation(bool resetToFirstFrame)
|
||
{
|
||
if (glassSeaAnimator == null) return;
|
||
|
||
if (resetToFirstFrame
|
||
&& glassSeaAnimator.runtimeAnimatorController != null
|
||
&& glassSeaAnimator.gameObject.activeInHierarchy)
|
||
{
|
||
var stateName = string.IsNullOrEmpty(_glassSeaCurrentStateName)
|
||
? glassSeaStateName
|
||
: _glassSeaCurrentStateName;
|
||
glassSeaAnimator.enabled = true;
|
||
glassSeaAnimator.Play(stateName, 0, 0f);
|
||
glassSeaAnimator.Update(0f);
|
||
}
|
||
|
||
glassSeaAnimator.enabled = false;
|
||
_glassSeaCurrentStateName = null;
|
||
}
|
||
|
||
private void FitGlassSeaToCameraCover()
|
||
{
|
||
FitSpriteToCameraCover(
|
||
glassSea,
|
||
_glassSeaOriginalLocalPosition,
|
||
_glassSeaOriginalLocalScale);
|
||
}
|
||
|
||
private void FitCodeSeaToCameraCover()
|
||
{
|
||
FitSpriteToCameraCover(
|
||
codeSea,
|
||
_codeSeaOriginalLocalPosition,
|
||
_codeSeaOriginalScale);
|
||
}
|
||
|
||
private void FitSpriteToCameraCover(
|
||
SpriteRenderer target,
|
||
Vector3 originalLocalPosition,
|
||
Vector3 originalLocalScale)
|
||
{
|
||
if (target == null || target.sprite == null) return;
|
||
|
||
var targetCamera = ResolveDoorCamera();
|
||
if (targetCamera == null) return;
|
||
|
||
var seaTransform = target.transform;
|
||
var parent = seaTransform.parent;
|
||
var depth = Vector3.Dot(seaTransform.position - targetCamera.transform.position, targetCamera.transform.forward);
|
||
depth = Mathf.Max(depth, targetCamera.nearClipPlane + 0.05f);
|
||
|
||
float viewWidth;
|
||
float viewHeight;
|
||
if (targetCamera.orthographic)
|
||
{
|
||
viewHeight = targetCamera.orthographicSize * 2f;
|
||
viewWidth = viewHeight * targetCamera.aspect;
|
||
}
|
||
else
|
||
{
|
||
var bottomLeft = targetCamera.ViewportToWorldPoint(new Vector3(0f, 0f, depth));
|
||
var bottomRight = targetCamera.ViewportToWorldPoint(new Vector3(1f, 0f, depth));
|
||
var topLeft = targetCamera.ViewportToWorldPoint(new Vector3(0f, 1f, depth));
|
||
viewWidth = Vector3.Distance(bottomLeft, bottomRight);
|
||
viewHeight = Vector3.Distance(bottomLeft, topLeft);
|
||
}
|
||
|
||
var spriteSize = target.sprite.bounds.size;
|
||
if (spriteSize.x <= 0.001f || spriteSize.y <= 0.001f) return;
|
||
|
||
var parentScale = parent != null ? parent.lossyScale : Vector3.one;
|
||
var localScale = Mathf.Max(
|
||
viewWidth / (spriteSize.x * Mathf.Max(Mathf.Abs(parentScale.x), 0.001f)),
|
||
viewHeight / (spriteSize.y * Mathf.Max(Mathf.Abs(parentScale.y), 0.001f)));
|
||
localScale *= glassSeaCoverOverscan;
|
||
|
||
var worldCenter = targetCamera.ViewportToWorldPoint(new Vector3(0.5f, 0.5f, depth));
|
||
var localCenter = parent != null ? parent.InverseTransformPoint(worldCenter) : worldCenter;
|
||
localCenter.z = originalLocalPosition.z;
|
||
|
||
seaTransform.localPosition = localCenter;
|
||
seaTransform.localScale = new Vector3(localScale, localScale, originalLocalScale.z);
|
||
}
|
||
|
||
private Camera ResolveDoorCamera()
|
||
{
|
||
foreach (var candidate in Camera.allCameras)
|
||
{
|
||
if (candidate != null
|
||
&& candidate.isActiveAndEnabled
|
||
&& candidate.gameObject.scene == gameObject.scene)
|
||
return candidate;
|
||
}
|
||
|
||
return Camera.main;
|
||
}
|
||
|
||
#endregion
|
||
|
||
private void SetBackgroundVisible(bool visible)
|
||
{
|
||
if (backgroundRenderers == null) return;
|
||
|
||
foreach (var backgroundRenderer in backgroundRenderers)
|
||
{
|
||
if (backgroundRenderer != null)
|
||
backgroundRenderer.enabled = visible;
|
||
}
|
||
}
|
||
|
||
private void ResetDoorShellScale()
|
||
{
|
||
if (_doorShellTransform != null)
|
||
_doorShellTransform.localScale = _doorShellOriginalScale;
|
||
}
|
||
|
||
private bool HasShapeReference()
|
||
{
|
||
return terminalShapeReference != null && terminalUI != null && _terminalUiTransform != null;
|
||
}
|
||
|
||
private void FitTerminalFrameToReference()
|
||
{
|
||
if (!HasShapeReference())
|
||
return;
|
||
|
||
var bounds = terminalShapeReference.bounds;
|
||
var targetLocalPosition = _terminalUiTransform.parent != null
|
||
? _terminalUiTransform.parent.InverseTransformPoint(bounds.center)
|
||
: bounds.center;
|
||
|
||
_terminalUiTransform.localPosition = new Vector3(
|
||
targetLocalPosition.x,
|
||
targetLocalPosition.y,
|
||
_terminalUiOriginalLocalPosition.z
|
||
);
|
||
|
||
Vector2 baseSize = terminalUI.EvaluateFrameSize(referenceSnapMorphProgress);
|
||
float targetWidth = Mathf.Max(0.01f, bounds.size.x + referenceSnapPadding.x);
|
||
float targetHeight = Mathf.Max(0.01f, bounds.size.y + referenceSnapPadding.y);
|
||
|
||
Vector3 parentLossyScale = _terminalUiTransform.parent != null ? _terminalUiTransform.parent.lossyScale : Vector3.one;
|
||
float parentScaleX = Mathf.Max(0.0001f, Mathf.Abs(parentLossyScale.x));
|
||
float parentScaleY = Mathf.Max(0.0001f, Mathf.Abs(parentLossyScale.y));
|
||
|
||
float localScaleX = targetWidth / Mathf.Max(0.0001f, baseSize.x * parentScaleX);
|
||
float localScaleY = targetHeight / Mathf.Max(0.0001f, baseSize.y * parentScaleY);
|
||
|
||
_terminalUiTransform.localScale = new Vector3(
|
||
Mathf.Sign(_terminalUiOriginalScale.x) * localScaleX,
|
||
Mathf.Sign(_terminalUiOriginalScale.y) * localScaleY,
|
||
_terminalUiOriginalScale.z
|
||
);
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
if (codeSea == null) return;
|
||
Gizmos.color = new Color(1f, 0.7f, 0f, 0.4f);
|
||
var bounds = codeSea.bounds;
|
||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||
|
||
if (terminalShapeReference != null)
|
||
{
|
||
Gizmos.color = new Color(1f, 1f, 1f, 0.45f);
|
||
var referenceBounds = terminalShapeReference.bounds;
|
||
Gizmos.DrawWireCube(referenceBounds.center, referenceBounds.size);
|
||
}
|
||
|
||
Gizmos.color = new Color(0f, 1f, 1f, 0.3f);
|
||
Vector3 termSize = new(
|
||
bounds.size.x * seaCollapseScale.x,
|
||
bounds.size.y * seaCollapseScale.y,
|
||
bounds.size.z
|
||
);
|
||
Gizmos.DrawWireCube(bounds.center, termSize);
|
||
}
|
||
#endif
|
||
}
|
||
}
|