using System.Collections; using AibisDream.Kit; using AibisDream.UI; using AibisDream.Utility; using DG.Tweening; using UnityEngine; namespace AibisDream { public class DreamDoorSystem : Singleton { [Header("Door Root")] [Tooltip("Door 根节点。默认仅 background 和 Frame 显示,整个 Door 初始隐藏。show_door 命令激活后显示。")] [SerializeField] private Transform doorRoot; [Header("Door Sprites")] [SerializeField] private SpriteRenderer glassSea; [SerializeField] private SpriteRenderer codeSea; [SerializeField] private SpriteRenderer flashOverlay; [Tooltip("仅放门的静态 background,不要把 glassSea/codeSea/overlay 拖进来。")] [SerializeField] private Renderer[] backgroundRenderers; [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.InOutCubic; [SerializeField] private FrameHidePhase frameHidePhase = FrameHidePhase.OnTerminal; [SerializeField] private float doorSnapDuration = 0.2f; [SerializeField] private float backgroundCutDelay = 0.04f; [SerializeField] private float preMorphHoldDuration = 0.16f; [SerializeField] private float postMorphBootDelay = 0.12f; [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"; 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 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); } SetDoorFrameSprite(hasSea: false); 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); } /// /// 进入“仔细听”的门近景。首次是白底门,之后直接显示门后的金色海浪。 /// public IEnumerator ShowListenView(bool showSea, float duration = 0f) { StopAllEffects(); EnsureDoorBaseVisible(showSea); ResetCodeSeaImmediate(); ResetTerminalImmediate(); Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(1f, 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) { var presentation = Day2SleepPresentationController.Instance; if (presentation == null) yield break; yield return presentation.AnimateLargePresentation(scale, alpha, duration); } public void SetListenCloseup(float scale, float alpha) { Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(scale, alpha); } /// /// 全白遮罩下调用的瞬时状态切换:隐藏门底和门框,仅显示全屏金色海浪。 /// public void ShowGlassSeaFullscreen() { if (doorRoot != null) doorRoot.gameObject.SetActive(true); SetBackgroundVisible(false); SetFrameVisible(false); ResetCodeSeaImmediate(); EnsureGlassSeaVisible(glassSeaStateName); } /// /// 全白遮罩下调用的瞬时状态切换:恢复门底和门框,金色海浪留在门后。 /// public void ShowGlassSeaFramed() { ResetCodeSeaImmediate(); EnsureDoorBaseVisible(hasSea: true); EnsureGlassSeaVisible(glassSeaFramedStateName); } /// /// 全白时切到无门的全屏 CodeSea;状态职责与 ShowGlassSeaFullscreen 一致。 /// public void ShowCodeSeaFullscreen() { if (doorRoot != null) doorRoot.gameObject.SetActive(true); var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea || !_codeSeaPlaybackActive; SetBackgroundVisible(false); SetFrameVisible(false); ResetGlassSeaImmediate(); EnsureCodeSeaVisible(restartPlayback); } /// /// 全白时恢复门框,CodeSea 保持显示在门内。 /// public void ShowCodeSeaFramed() { var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea || !_codeSeaPlaybackActive; ResetGlassSeaImmediate(); EnsureDoorBaseVisible(hasSea: false); 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) SetFrameVisible(false); 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(); if (terminalUI != null) { terminalUI.ResetVisualState(); terminalUI.MorphProgress = 0f; terminalUI.Alpha = 1f; } if (_terminalUiTransform != null) { _terminalUiTransform.localPosition = _terminalUiOriginalLocalPosition; _terminalUiTransform.localScale = _terminalUiOriginalScale; } SetBackgroundVisible(false); SetFrameVisible(false); 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; var morphTween = DOTween.To(() => morph, v => { morph = v; terminalUI.MorphProgress = v; }, 1f, 0.82f).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(); } 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() { 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); SetFrameVisible(true); SetDoorFrameSprite(hasSea: false); 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); Day2SleepPresentationController.Instance?.HideLargePresentationImmediate(); _presentationPhase = DoorPresentationPhase.Hidden; } #endregion #region Presentation State private void EnsureDoorBaseVisible(bool hasSea = false) { if (doorRoot != null) doorRoot.gameObject.SetActive(true); SetBackgroundVisible(true); SetFrameVisible(true); SetDoorFrameSprite(hasSea); ResetDoorShellScale(); } 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 SetFrameVisible(bool visible) { if (frameRenderers == null) return; foreach (var frameRenderer in frameRenderers) { if (frameRenderer != null) frameRenderer.enabled = visible; } } private void SetDoorFrameSprite(bool hasSea) { var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite; if (targetSprite == null) return; if (frameRenderers != null) { foreach (var frameRenderer in frameRenderers) { if (frameRenderer is SpriteRenderer spriteRenderer) spriteRenderer.sprite = targetSprite; } } Day2SleepPresentationController.Instance?.SetLargeSpriteImmediate(targetSprite); } 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 } }