using System; using System.Collections; using System.Collections.Generic; using DG.Tweening; using Shapes; using TMPro; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace AibisDream.UI { /// /// 梦境命令窗口:全部用 Shapes 即时模式绘制,无需手动配置任何 Canvas 或 UI 组件。 /// 挂在 Door 节点(或其子节点)上即可。 /// [ExecuteAlways] public class DreamTerminalUI : ImmediateModeShapeDrawer { #region 样式参数 [Header("尺寸(世界单位)")] [SerializeField] private Vector2 terminalSize = new(6.8f, 4.2f); [SerializeField] private Vector2 morphStartSize = new(8.6f, 5.6f); [Header("琥珀色调色盘")] [SerializeField] private Color colAmberMain = new(1f, 0.69f, 0f, 1f); // #ffb000 [SerializeField] private Color colAmberDim = new(0.72f, 0.52f, 0.04f, 1f); // #b8860b [SerializeField] private Color colWhiteGlow = new(1f, 1f, 1f, 1f); [SerializeField] private Color colBg = new(0.02f, 0.02f, 0.02f, 0.96f); [Header("布局")] [SerializeField] private float borderThickness = 0.05f; [SerializeField] private float padding = 0.3f; [SerializeField] private float lineHeight = 0.3f; [SerializeField] private float fontSize = 0.24f; [SerializeField] private float inputAreaHeight = 0.58f; [SerializeField] private int maxVisibleLines = 9; [Header("扫描线")] [SerializeField] private float scanlineSpacing = 0.07f; [SerializeField] private float scanlineAlpha = 0.08f; [Header("光标")] [SerializeField] private float cursorBlinkRate = 0.5f; [SerializeField] private Vector2 cursorSize = new(0.14f, 0.24f); [Header("形变")] [Range(0f, 1f)] [SerializeField] private float morphProgress = 0f; [Header("编辑器预览")] [SerializeField] private bool previewInEditor = true; [Range(0f, 1f)] [SerializeField] private float previewAlpha = 1f; [Range(0f, 1f)] [SerializeField] private float previewMorphProgress = 1f; [SerializeField] private PreviewStage previewStage = PreviewStage.WaitingInput; [SerializeField] private bool animatePreviewCursor = true; [SerializeField] private string previewInputBuffer = "false"; [TextArea(3, 8)] [SerializeField] private string previewLines = ">> WARNING: TRAP DETECTED.\n" + ">> SYSTEM: Reality_Engine.sys stopped.\n" + ">> SYSTEM: Rendering underlying code...\n" + ">> LOADING: Ocean_Simulation.sh ... OK\n" + "root@sys:~$ print_string(\"海是存在的吗?\")"; [SerializeField] private string previewTypingLine = ""; [Header("交互")] [SerializeField] private int maxInputLength = 20; [SerializeField] private string promptString = "> bit seaExists = "; [SerializeField] private string onCompleteNode = "苏醒"; [Header("启动日志")] [SerializeField] private List bootSequence = new() { new BootLogEntry { text = ">> WARNING: TRAP DETECTED.", style = LogStyle.Alert, delayAfter = 0.5f }, new BootLogEntry { text = ">> SYSTEM: Reality_Engine.sys stopped.", style = LogStyle.Dim, delayAfter = 0.2f }, new BootLogEntry { text = ">> SYSTEM: Rendering underlying code...", style = LogStyle.Dim, delayAfter = 0.2f }, new BootLogEntry { text = ">> LOADING: Ocean_Simulation.sh ... OK", style = LogStyle.Dim, delayAfter = 0.8f }, new BootLogEntry { text = "root@sys:~$ print_string(\"海是存在的吗?\")", style = LogStyle.Highlight, delayAfter = 0.5f }, }; #endregion #region 状态 private enum TerminalPhase { Idle, BootSequence, WaitingInput, Confirming, EndAnimation, Done } private TerminalPhase _phase = TerminalPhase.Idle; // 可见文本行及其颜色 private readonly List _lines = new(); private readonly List _lineColors = new(); // 打字机 private string _typingTarget = ""; private string _typingCurrent = ""; private bool _typingDone = false; private Color _typingColor; private Coroutine _typewriterCoroutine; // 输入 private string _inputBuffer = ""; private bool _cursorVisible = true; private Coroutine _cursorBlinkCoroutine; private Coroutine _crashSpamCoroutine; private double _editorLastRepaintTime; // 全局 alpha(用于 DreamDoorSystem 淡入淡出) public float Alpha { get; set; } = 0f; public float MorphProgress { get => morphProgress; set => morphProgress = Mathf.Clamp01(value); } #endregion #region 生命周期 private void Awake() { Alpha = 0f; MorphProgress = 0f; _phase = TerminalPhase.Idle; } private void Update() { if (!Application.isPlaying) { UpdateEditorPreview(); return; } if (_phase == TerminalPhase.WaitingInput) HandleTypingInput(); else if (_phase == TerminalPhase.Confirming) HandleConfirmInput(); } public override void OnDisable() { base.OnDisable(); _phase = TerminalPhase.Idle; StopAllCoroutines(); } public override void OnEnable() { base.OnEnable(); if (!Application.isPlaying) ApplyEditorPreviewState(); } private void OnValidate() { MorphProgress = morphProgress; if (!Application.isPlaying) ApplyEditorPreviewState(); } #endregion #region 公共接口 public void Begin() { if (_phase != TerminalPhase.Idle) return; _lines.Clear(); _lineColors.Clear(); _inputBuffer = ""; MorphProgress = 1f; StartCoroutine(RunBootSequence()); } public void ForceEnd() { StopAllCoroutines(); _phase = TerminalPhase.Done; } public void ResetVisualState() { StopAllCoroutines(); _phase = TerminalPhase.Idle; _lines.Clear(); _lineColors.Clear(); _typingTarget = ""; _typingCurrent = ""; _typingDone = false; _inputBuffer = ""; _cursorVisible = false; MorphProgress = 0f; } [ContextMenu("Preview/Frame Only")] private void PreviewFrameOnly() { previewInEditor = true; previewStage = PreviewStage.FrameOnly; previewAlpha = 1f; previewMorphProgress = 0.65f; ApplyEditorPreviewState(); } [ContextMenu("Preview/Waiting Input")] private void PreviewWaitingInput() { previewInEditor = true; previewStage = PreviewStage.WaitingInput; previewAlpha = 1f; previewMorphProgress = 1f; ApplyEditorPreviewState(); } [ContextMenu("Preview/Clear")] private void ClearPreview() { previewInEditor = false; ApplyEditorPreviewState(); } #endregion #region Shapes 绘制(DrawShapes 覆写) public override void DrawShapes(Camera cam) { if (Alpha <= 0.01f) return; float frameProgress = Mathf.SmoothStep(0f, 1f, MorphProgress); float textReveal = Mathf.InverseLerp(0.78f, 1f, frameProgress); Vector2 currentSize = Vector2.Lerp(morphStartSize, terminalSize, frameProgress); using (Draw.Command(cam)) { Draw.BlendMode = ShapesBlendMode.Transparent; Draw.ZTest = UnityEngine.Rendering.CompareFunction.Always; using (Draw.MatrixScope) { Draw.Matrix = transform.localToWorldMatrix; DrawBackground(currentSize, frameProgress); DrawScanlines(currentSize, frameProgress); DrawBorder(currentSize, frameProgress); DrawLogLines(currentSize, textReveal); DrawInputArea(currentSize, textReveal); } } } // ----- 背景 ----- private void DrawBackground(Vector2 currentSize, float frameProgress) { Color bg = colBg; bg.a *= Alpha * Mathf.Lerp(0.18f, 1f, frameProgress); Draw.Rectangle(Vector3.zero, Quaternion.identity, currentSize, bg); } // ----- CRT 扫描线 ----- private void DrawScanlines(Vector2 currentSize, float frameProgress) { float scanReveal = Mathf.InverseLerp(0.35f, 1f, frameProgress); if (scanReveal <= 0f) return; float halfH = currentSize.y * 0.5f; float halfW = currentSize.x * 0.5f; Color scanCol = new Color(0f, 0f, 0f, scanlineAlpha * Alpha * scanReveal); Draw.Thickness = scanlineSpacing * 0.5f; Draw.LineGeometry = LineGeometry.Flat2D; // 横向扫描线(每隔 spacing 画一条暗线) float y = -halfH + scanlineSpacing; while (y < halfH) { Draw.Line(new Vector3(-halfW, y, 0f), new Vector3(halfW, y, 0f), scanCol); y += scanlineSpacing * 2f; } } // ----- 琥珀色边框 ----- private void DrawBorder(Vector2 currentSize, float frameProgress) { float borderReveal = Mathf.InverseLerp(0.15f, 0.78f, frameProgress); if (borderReveal <= 0f) return; Color borderCol = colAmberMain; borderCol.a *= Alpha * borderReveal; Draw.RectangleBorder(Vector3.zero, Quaternion.identity, currentSize, borderThickness, borderCol); float halfW = currentSize.x * 0.5f; float halfH = currentSize.y * 0.5f; float cornerLen = Mathf.Lerp(0.25f, 0.6f, borderReveal); float inset = borderThickness * 0.6f; Draw.Thickness = borderThickness * 0.75f; Draw.LineGeometry = LineGeometry.Flat2D; DrawCorner(new Vector2(-halfW + inset, halfH - inset), Vector2.right, Vector2.down, cornerLen, borderCol); DrawCorner(new Vector2( halfW - inset, halfH - inset), Vector2.left, Vector2.down, cornerLen, borderCol); DrawCorner(new Vector2(-halfW + inset, -halfH + inset), Vector2.right, Vector2.up, cornerLen, borderCol); DrawCorner(new Vector2( halfW - inset, -halfH + inset), Vector2.left, Vector2.up, cornerLen, borderCol); float separatorReveal = Mathf.InverseLerp(0.45f, 0.95f, frameProgress); if (separatorReveal > 0f) { Color divCol = colAmberDim; divCol.a *= Alpha * separatorReveal; float botY = -currentSize.y * 0.5f + inputAreaHeight; float lineHalfWidth = (halfW - padding) * separatorReveal; Draw.Thickness = 0.015f; Draw.Line( new Vector3(-lineHalfWidth, botY, 0f), new Vector3(lineHalfWidth, botY, 0f), divCol ); } } // ----- 日志文本区 ----- private void DrawLogLines(Vector2 currentSize, float textReveal) { if (textReveal <= 0f) return; float halfW = currentSize.x * 0.5f; float topY = currentSize.y * 0.5f - padding; // 最多显示 maxVisibleLines 行 int startIdx = Mathf.Max(0, _lines.Count - maxVisibleLines); for (int i = startIdx; i < _lines.Count; i++) { int row = i - startIdx; float y = topY - row * lineHeight; Color c = _lineColors.Count > i ? _lineColors[i] : colAmberMain; c.a *= Alpha * textReveal; Draw.FontSize = fontSize; Draw.Text( new Vector3(-halfW + padding, y, 0f), Quaternion.identity, _lines[i], TextAlign.TopLeft, c ); } // 正在打字的当前行 if (!string.IsNullOrEmpty(_typingCurrent)) { int row = Mathf.Min(_lines.Count - startIdx, maxVisibleLines - 1); float y = topY - row * lineHeight; Color c = _typingColor; c.a *= Alpha * textReveal; Draw.FontSize = fontSize; Draw.Text( new Vector3(-halfW + padding, y, 0f), Quaternion.identity, _typingCurrent, TextAlign.TopLeft, c ); } } // ----- 输入区 ----- private void DrawInputArea(Vector2 currentSize, float textReveal) { if (textReveal <= 0f) return; if (_phase != TerminalPhase.WaitingInput && _phase != TerminalPhase.Confirming) return; float halfW = currentSize.x * 0.5f; float botY = -currentSize.y * 0.5f + inputAreaHeight; // Prompt + 用户输入 string display = promptString + _inputBuffer; Color promptCol = colAmberMain; promptCol.a *= Alpha * textReveal; float textY = botY - lineHeight * 0.6f; Draw.FontSize = fontSize; Draw.Text( new Vector3(-halfW + padding, textY, 0f), Quaternion.identity, display, TextAlign.TopLeft, promptCol ); // 闪烁光标 if (_cursorVisible && _phase == TerminalPhase.WaitingInput) { // 估算文本宽度(粗略按字符数 × fontSize × 0.6) float textWidth = display.Length * fontSize * 0.6f; Vector3 cursorPos = new Vector3(-halfW + padding + textWidth, textY - cursorSize.y * 0.3f, 0f); Color cursorCol = colAmberMain; cursorCol.a *= Alpha * textReveal; Draw.Rectangle(cursorPos, Quaternion.identity, cursorSize, cursorCol); } } private static void DrawCorner(Vector2 pivot, Vector2 horizontalDir, Vector2 verticalDir, float length, Color color) { Draw.Line( new Vector3(pivot.x, pivot.y, 0f), new Vector3(pivot.x + horizontalDir.x * length, pivot.y + horizontalDir.y * length, 0f), color ); Draw.Line( new Vector3(pivot.x, pivot.y, 0f), new Vector3(pivot.x + verticalDir.x * length, pivot.y + verticalDir.y * length, 0f), color ); } #endregion #region 打字机 private void AddLine(string text, Color color) { _lines.Add(text); _lineColors.Add(color); } private void AppendToLastLine(string text) { if (_lines.Count > 0) _lines[^1] += text; } private IEnumerator TypewriterLine(string text, Color color, float charDelay = 0.025f) { _typingTarget = text; _typingCurrent = ""; _typingColor = color; _typingDone = false; foreach (char c in text) { _typingCurrent += c; yield return new WaitForSeconds(charDelay); } // 提交为已完成行 AddLine(_typingCurrent, color); _typingCurrent = ""; _typingTarget = ""; _typingDone = true; } private IEnumerator WaitForTypewriter() { while (!_typingDone) yield return null; } #endregion #region Boot Sequence private IEnumerator RunBootSequence() { _phase = TerminalPhase.BootSequence; yield return new WaitForSeconds(0.3f); foreach (var entry in bootSequence) { Color c = StyleToColor(entry.style); _typewriterCoroutine = StartCoroutine(TypewriterLine(entry.text, c)); yield return WaitForTypewriter(); if (entry.delayAfter > 0f) yield return new WaitForSeconds(entry.delayAfter); } yield return new WaitForSeconds(0.3f); EnterInputPhase(); } private Color StyleToColor(LogStyle style) => style switch { LogStyle.Alert => colWhiteGlow, LogStyle.Highlight => colWhiteGlow, LogStyle.Dim => colAmberDim, _ => colAmberMain }; #endregion #region Input Phase private void EnterInputPhase() { _phase = TerminalPhase.WaitingInput; _inputBuffer = ""; StartCursorBlink(); } private void HandleTypingInput() { foreach (char c in Input.inputString) { if (c == '\b') { if (_inputBuffer.Length > 0) _inputBuffer = _inputBuffer[..^1]; } else if (c == '\n' || c == '\r') { if (_inputBuffer.Length > 0) EnterConfirmPhase(); return; } else if (!char.IsControl(c) && _inputBuffer.Length < maxInputLength) { _inputBuffer += c; } } } #endregion #region Confirm Phase private void EnterConfirmPhase() { _phase = TerminalPhase.Confirming; StopCursorBlink(); AddLine(">> Submit Reality Check? (Y/N)", colWhiteGlow); } private void HandleConfirmInput() { if (Input.GetKeyDown(KeyCode.Y)) { _phase = TerminalPhase.EndAnimation; StartCoroutine(HandleSubmission(_inputBuffer)); } else if (Input.GetKeyDown(KeyCode.N)) { _phase = TerminalPhase.WaitingInput; StartCursorBlink(); } } #endregion #region End Animation private IEnumerator HandleSubmission(string input) { string val = input.Trim().ToLower(); bool isFalse = val is "false" or "0" or "no" or "不存在"; if (isFalse) yield return PlayShutdownEnding(); else yield return PlayCrashEnding(); _phase = TerminalPhase.Done; yield return new WaitForSeconds(1f); DialogController.Instance.StartDialogNode(onCompleteNode); } private IEnumerator PlayShutdownEnding() { AddLine(">> FALSE. SHUTTING DOWN...", colWhiteGlow); yield return new WaitForSeconds(1.5f); yield return DreamDoorSystem.Instance.PlayShutdownEffect(); } private IEnumerator PlayCrashEnding() { AddLine(">> TRUE. ERROR: OVERFLOW.", colWhiteGlow); yield return new WaitForSeconds(0.8f); DreamDoorSystem.Instance.PlayCrashEffect(); _crashSpamCoroutine = StartCoroutine(SpamErrorLines()); yield return new WaitForSeconds(3f); if (_crashSpamCoroutine != null) StopCoroutine(_crashSpamCoroutine); DreamDoorSystem.Instance.StopAllEffects(); } private IEnumerator SpamErrorLines() { while (true) { string hex = UnityEngine.Random.Range(0, 0xFFFF).ToString("x4"); AddLine($"0x{hex} SYSTEM FAILURE", colAmberDim); yield return new WaitForSeconds(0.05f); } } #endregion #region 光标闪烁 private void StartCursorBlink() { _cursorVisible = true; _cursorBlinkCoroutine = StartCoroutine(BlinkCursor()); } private void StopCursorBlink() { if (_cursorBlinkCoroutine != null) StopCoroutine(_cursorBlinkCoroutine); _cursorVisible = false; } private IEnumerator BlinkCursor() { while (true) { _cursorVisible = !_cursorVisible; yield return new WaitForSeconds(cursorBlinkRate); } } #endregion #region 数据结构 [Serializable] public class BootLogEntry { public string text; public LogStyle style; public float delayAfter; } private enum PreviewStage { Hidden, FrameOnly, BootText, WaitingInput, Confirming } public enum LogStyle { Normal, Alert, Highlight, Dim } #endregion #if UNITY_EDITOR private void UpdateEditorPreview() { ApplyEditorPreviewState(); if (previewInEditor) { EditorApplication.QueuePlayerLoopUpdate(); double now = EditorApplication.timeSinceStartup; if (now - _editorLastRepaintTime > 0.08d) { _editorLastRepaintTime = now; SceneView.RepaintAll(); } } } private void ApplyEditorPreviewState() { if (Application.isPlaying) return; if (!previewInEditor) { Alpha = 0f; MorphProgress = 0f; _phase = TerminalPhase.Idle; _lines.Clear(); _lineColors.Clear(); _typingCurrent = ""; _inputBuffer = ""; _cursorVisible = false; return; } Alpha = previewAlpha; MorphProgress = previewMorphProgress; _lines.Clear(); _lineColors.Clear(); if (!string.IsNullOrWhiteSpace(previewLines)) { var previewEntries = previewLines.Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var line in previewEntries) AddPreviewLine(line); } _typingCurrent = previewTypingLine ?? ""; _typingColor = colAmberMain; _inputBuffer = previewInputBuffer ?? ""; switch (previewStage) { case PreviewStage.Hidden: Alpha = 0f; MorphProgress = 0f; _phase = TerminalPhase.Idle; _cursorVisible = false; break; case PreviewStage.FrameOnly: _phase = TerminalPhase.Idle; _typingCurrent = ""; _cursorVisible = false; break; case PreviewStage.BootText: _phase = TerminalPhase.BootSequence; _cursorVisible = false; break; case PreviewStage.WaitingInput: _phase = TerminalPhase.WaitingInput; _cursorVisible = animatePreviewCursor ? ((EditorApplication.timeSinceStartup % (cursorBlinkRate * 2f)) < cursorBlinkRate) : true; break; case PreviewStage.Confirming: _phase = TerminalPhase.Confirming; _cursorVisible = false; AddPreviewLine(">> Submit Reality Check? (Y/N)"); break; } } private void AddPreviewLine(string text) { Color color = text.Contains("WARNING") || text.Contains("FALSE") || text.Contains("ERROR") ? colWhiteGlow : text.Contains("SYSTEM") || text.Contains("LOADING") ? colAmberDim : colAmberMain; _lines.Add(text); _lineColors.Add(color); } #endif } }