Files
aibis-dream/Assets/Scripts/SceneManagement/DreamDoorSystem.cs
T

1039 lines
38 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("变形为终端开始时并行播放的 TimelineTimelineCenter 名称,如 姐姐/头痛;留空则不播)")]
[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
}
}