using System.Collections;
using AibisDream.Kit;
using AibisDream.UI;
using AibisDream.Utility;
using DG.Tweening;
using UnityEngine;
namespace AibisDream
{
///
/// Day2 门演出。可见门框与门后内容分层如下,勿混用:
///
/// - 可见门框(近景):只用 SpriteShowcase 大图(白色门框/金色门框)。
/// 缩放/透明度走 set_door_closeup / tween_door_closeup,不要再 show_large_sprite「门框特写」叠一层。
/// - Door/background:HDR 白底或门后内容,从大图透明洞透出。
/// - Door/Frame:场景里保持 inactive,缩放与近景大图不同;仅作 terminalShapeReference / 贴图同步,不要 SetActive 当第二层门框。
/// - 远景整门立绘:hide_dream_door + show_large_sprite「D2S门」,不走本系统 Frame。
///
///
public class DreamDoorSystem : Singleton
{
[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();
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;
}
///
/// 显示普通门。该命令是一个完整状态切换,会清掉之前残留的海景、代码海浪和终端。
///
public IEnumerator ShowDoor(float duration = 0f)
{
StopAllEffects();
EnsureDoorBaseVisible(hasSea: false);
ResetGlassSeaImmediate();
ResetCodeSeaImmediate();
ResetTerminalImmediate();
_presentationPhase = DoorPresentationPhase.Door;
if (duration > 0f)
yield return new WaitForSeconds(duration);
}
///
/// 门近景:大图显示白/金色门框 + Door 内 background/海浪。
/// Yarn 无需再先 show_large_sprite「门框特写」(会被本命令覆盖)。
///
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);
}
///
/// 隐藏整个门
///
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;
}
///
/// 只调整近景门图片的缩放与透明度,不包含白幕、Bloom 或海浪状态切换。
///
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);
}
/// 异步近景;再次调用 set/tween/show_framed 时会取消上一次。
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();
}
///
/// 全白时:隐藏门底与大图门框,仅全屏金色海浪。
///
public void ShowGlassSeaFullscreen()
{
if (doorRoot != null)
doorRoot.gameObject.SetActive(true);
SetBackgroundVisible(false);
HideLargeFrame();
ResetCodeSeaImmediate();
EnsureGlassSeaVisible(glassSeaStateName);
}
///
/// 全白时:恢复大图门框,金色海浪留在门后。
///
public void ShowGlassSeaFramed()
{
StopCloseupCoroutine();
ResetCodeSeaImmediate();
EnsureDoorBaseVisible(hasSea: true);
ShowLargeFrame(hasSea: true, scale: 1f, alpha: 1f);
EnsureGlassSeaVisible(glassSeaFramedStateName);
}
///
/// 全白时:隐藏门底与大图门框,全屏 CodeSea。
///
public void ShowCodeSeaFullscreen()
{
if (doorRoot != null)
doorRoot.gameObject.SetActive(true);
var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea
|| !_codeSeaPlaybackActive;
SetBackgroundVisible(false);
HideLargeFrame();
ResetGlassSeaImmediate();
EnsureCodeSeaVisible(restartPlayback);
}
///
/// 全白时:恢复大图白色门框(scale=1),CodeSea 留在门内。
/// 会取消进行中的 tween_door_closeup_async,避免停在 4x 放大。
///
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()
: 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
///
/// 打开 Door 根与门后背景;不同时显示场景 Frame(近景门框只走大图)。
///
private void EnsureDoorBaseVisible(bool hasSea = false)
{
if (doorRoot != null)
doorRoot.gameObject.SetActive(true);
// 梦境「遮罩」是纯黑全屏;不关会从门框透明洞透出黑底,盖住 HDR 白光。
SpriteShowcase.Instance?.HideBackgroundImmediate();
SetBackgroundVisible(true);
KeepSceneFrameInactive();
SyncSceneFrameSprite(hasSea);
ResetDoorShellScale();
}
/// 可见门框:大图贴白/金色门框并设缩放透明度。
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();
}
/// 只同步场景 Frame 贴图,不激活(供 morph 参考一致)。
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;
}
}
///
/// 场景 Frame 必须保持 inactive。其 localScale≈0.73,若被激活会叠出一层「缩小门框」。
///
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
}
}