using System; using System.Collections; using System.Collections.Generic; using Shapes; using TMPro; using UnityEngine; using UnityEngine.Rendering; #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.4f, 4.1f); [SerializeField] private Vector2 morphStartSize = new(4.9f, 7.6f); [Header("复古像素风格")] [SerializeField] private TMP_FontAsset terminalFont; [SerializeField] private Color colAmberMain = new(0.81f, 0.81f, 0.81f, 1f); [SerializeField] private Color colAmberDim = new(0.36f, 0.36f, 0.36f, 1f); [SerializeField] private Color colWhiteGlow = new(0.95f, 0.95f, 0.95f, 1f); [SerializeField] private Color colBg = new(0.02f, 0.02f, 0.02f, 0.97f); [SerializeField] private Color colAlert = new(0.92f, 0.26f, 0.26f, 1f); [Header("布局")] [SerializeField] private float borderThickness = 0.036f; [SerializeField] private float padding = 0.34f; [SerializeField] private float lineHeight = 0.3f; [SerializeField] private float fontSize = 0.22f; [SerializeField] private float inputAreaHeight = 0.58f; [SerializeField] private int maxVisibleLines = 9; [Header("CRT 活性")] [SerializeField] private float borderGhostOffset = 0.018f; [SerializeField] private float borderGhostAlpha = 0.08f; [SerializeField] private float borderFlickerSpeed = 1.3f; [SerializeField] private float borderFlickerAmount = 0.05f; [Range(0f, 0.45f)] [SerializeField] private float borderGapRatio = 0.018f; [SerializeField] private Vector2 textGhostOffset = new(0.015f, -0.008f); [Range(0f, 1f)] [SerializeField] private float textGhostAlpha = 0.12f; [Header("复古布局")] [SerializeField] private string terminalTitle = "AIBIS-DREAM DOS v0.2"; [SerializeField] private float headerBandHeight = 0.34f; [SerializeField] private float footerBandHeight = 0.42f; [Header("光标")] [SerializeField] private float cursorBlinkRate = 0.5f; [SerializeField] private Vector2 cursorSize = new(0.022f, 0.24f); [SerializeField] private float cursorGlowWidth = 0.014f; [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("速度(可调)")] [Tooltip("打字机每字延迟(秒),越小越快")] [Range(0.005f, 0.15f)] [SerializeField] private float typewriterCharDelay = 0.025f; [Tooltip("启动日志整体速度倍率,1=正常,2=两倍快,0.5=半速")] [Range(0.25f, 4f)] [SerializeField] private float bootSpeedMultiplier = 1f; [Header("启动日志")] [SerializeField] private List bootSequence = new() { new BootLogEntry { text = "root@sys:~$ dream.render --stop --session 0742", style = LogStyle.Dim, delayAfter = 0.15f }, new BootLogEntry { text = "[ OK ] Dream renderer stopped.", style = LogStyle.Normal, delayAfter = 0.25f }, new BootLogEntry { text = "[ RAW ] Untitled_Greybox_0 exposed.", style = LogStyle.Normal, delayAfter = 0.35f }, new BootLogEntry { text = "root@sys:~$ trace --tail /log/dream_0742.rec", style = LogStyle.Dim, delayAfter = 0.15f }, new BootLogEntry { text = "[0005] audio.play(\"sea_ambient\", source=door.rear)", style = LogStyle.Dim, delayAfter = 0.1f }, new BootLogEntry { text = "[0006] memory.project(\"glass_sea\")", style = LogStyle.Dim, delayAfter = 0.1f }, new BootLogEntry { text = "[ WARN ] memory source unresolved; overflow detected.", style = LogStyle.Alert, delayAfter = 0.4f }, 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 _crashSpamCoroutine; private double _editorLastRepaintTime; #if UNITY_EDITOR private GUIStyle _editorPreviewLabelStyle; #endif private IMECompositionMode _previousImeMode = IMECompositionMode.Auto; private bool _imeOverridden; private bool _compositionWasActive; // 全局 alpha(用于 DreamDoorSystem 淡入淡出) public float Alpha { get; set; } = 0f; public float MorphProgress { get => morphProgress; set => morphProgress = Mathf.Clamp01(value); } #endregion #region 生命周期 private void Awake() { EnsureDefaultFontReference(); Alpha = 0f; MorphProgress = 0f; _phase = TerminalPhase.Idle; } private void Update() { if (_phase == TerminalPhase.WaitingInput) HandleTypingInput(); else if (_phase == TerminalPhase.Confirming) HandleConfirmInput(); } public override void OnDisable() { #if UNITY_EDITOR SceneView.duringSceneGui -= OnEditorSceneGui; EditorApplication.update -= EditorPreviewUpdate; if (!Application.isPlaying) { DrawCommand.ClearAllCommands(); _phase = TerminalPhase.Idle; StopAllCoroutines(); return; } #endif base.OnDisable(); _phase = TerminalPhase.Idle; RestoreImeMode(); StopAllCoroutines(); } public override void OnEnable() { EnsureDefaultFontReference(); #if UNITY_EDITOR if (!Application.isPlaying) { SceneView.duringSceneGui -= OnEditorSceneGui; SceneView.duringSceneGui += OnEditorSceneGui; EditorApplication.update -= EditorPreviewUpdate; EditorApplication.update += EditorPreviewUpdate; EditorApply(); return; } #endif base.OnEnable(); } private void OnValidate() { EnsureDefaultFontReference(); MorphProgress = morphProgress; #if UNITY_EDITOR if (!Application.isPlaying) EditorApply(); #endif } #endregion #region 公共接口 public void Begin() { if (_phase != TerminalPhase.Idle) return; _lines.Clear(); _lineColors.Clear(); _inputBuffer = ""; MorphProgress = 1f; StartCoroutine(RunBootSequence()); } public void ForceEnd() { StopAllCoroutines(); RestoreImeMode(); _phase = TerminalPhase.Done; } public void ResetVisualState() { StopAllCoroutines(); RestoreImeMode(); _phase = TerminalPhase.Idle; _lines.Clear(); _lineColors.Clear(); _typingTarget = ""; _typingCurrent = ""; _typingDone = false; _inputBuffer = ""; _cursorVisible = false; MorphProgress = 0f; } public Vector2 EvaluateFrameSize(float progress) { float frameProgress = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(progress)); return Vector2.Lerp(morphStartSize, terminalSize, frameProgress); } [ContextMenu("Preview/Frame Only")] private void PreviewFrameOnly() { previewInEditor = true; previewStage = PreviewStage.FrameOnly; previewAlpha = 1f; previewMorphProgress = 0.65f; EditorApply(); } [ContextMenu("Preview/Waiting Input")] private void PreviewWaitingInput() { previewInEditor = true; previewStage = PreviewStage.WaitingInput; previewAlpha = 1f; previewMorphProgress = 1f; EditorApply(); } [ContextMenu("Preview/Clear")] private void ClearPreview() { previewInEditor = false; EditorApply(); } #endregion #region Shapes 绘制(DrawShapes 覆写) public override void DrawShapes(Camera cam) { #if UNITY_EDITOR // 编辑模式预览改走 SceneView Handles,URP 下 Shapes 文本池不会回收。 if (!Application.isPlaying) return; #endif if (Alpha <= 0.01f) return; float frameProgress = Mathf.SmoothStep(0f, 1f, MorphProgress); float textReveal = frameProgress >= 0.999f ? 1f : 0f; Vector2 currentSize = Vector2.Lerp(morphStartSize, terminalSize, frameProgress); using (Draw.Command(cam)) { Draw.BlendMode = ShapesBlendMode.Transparent; Draw.ZTest = UnityEngine.Rendering.CompareFunction.Always; Draw.Font = terminalFont; Draw.FontSize = fontSize; using (Draw.MatrixScope) { Draw.Matrix = transform.localToWorldMatrix; DrawBackground(currentSize, frameProgress); DrawBorder(currentSize, frameProgress, textReveal); DrawLogLines(currentSize, textReveal); DrawInputArea(currentSize, textReveal); } } } // ----- 背景 ----- private void DrawBackground(Vector2 currentSize, float frameProgress) { float pulse = Mathf.Lerp(0.96f, 1.02f, SampleNoise(0.11f, 0.55f)); Color bg = colBg; bg.a *= Alpha * pulse; Draw.Rectangle(Vector3.zero, Quaternion.identity, currentSize, bg); float halfH = currentSize.y * 0.5f; Color vignette = Color.black; vignette.a = Alpha * 0.08f; Draw.Rectangle(new Vector3(0f, halfH - 0.18f, 0f), Quaternion.identity, new Vector2(currentSize.x, 0.36f), vignette); Draw.Rectangle(new Vector3(0f, -halfH + 0.18f, 0f), Quaternion.identity, new Vector2(currentSize.x, 0.36f), vignette); } // ----- 琥珀色边框 ----- private void DrawBorder(Vector2 currentSize, float frameProgress, float textReveal) { DrawTerminalFrame(currentSize, frameProgress); DrawDosChrome(currentSize, frameProgress, textReveal); } private void DrawTerminalFrame(Vector2 currentSize, float frameProgress) { float frameReveal = 1f; if (frameReveal <= 0f) return; float halfW = currentSize.x * 0.5f - padding * 0.48f; float halfH = currentSize.y * 0.5f - padding * 0.42f; float topY = halfH; float botY = -halfH; float leftX = -halfW; float rightX = halfW; Color outer = colWhiteGlow; outer.a *= Alpha * frameReveal * 0.58f; Color inner = colWhiteGlow; inner.a *= Alpha * frameReveal * 0.11f; DrawContinuousLine(new Vector3(leftX, topY, 0f), new Vector3(rightX, topY, 0f), outer, borderThickness * 0.84f, 0.51f); DrawContinuousLine(new Vector3(leftX, botY, 0f), new Vector3(rightX, botY, 0f), outer, borderThickness * 0.84f, 0.58f); DrawContinuousLine(new Vector3(leftX, botY, 0f), new Vector3(leftX, topY, 0f), outer, borderThickness * 0.72f, 0.64f); DrawContinuousLine(new Vector3(rightX, botY, 0f), new Vector3(rightX, topY, 0f), outer, borderThickness * 0.72f, 0.69f); float innerInset = borderThickness * 2.8f; DrawContinuousLine( new Vector3(leftX + innerInset, topY - innerInset, 0f), new Vector3(rightX - innerInset, topY - innerInset, 0f), inner, borderThickness * 0.24f, 0.73f ); DrawContinuousLine( new Vector3(leftX + innerInset, botY + innerInset, 0f), new Vector3(rightX - innerInset, botY + innerInset, 0f), inner, borderThickness * 0.2f, 0.79f ); } private void DrawDosChrome(Vector2 currentSize, float frameProgress, float textReveal) { float chromeReveal = 1f; if (chromeReveal <= 0f) return; float halfW = currentSize.x * 0.5f; float halfH = currentSize.y * 0.5f; float frameHalfW = currentSize.x * 0.5f - padding * 0.48f; float leftX = -halfW + padding * 0.9f; float rightX = halfW - padding * 0.9f; float lineLeftX = -frameHalfW; float lineRightX = frameHalfW; float topY = halfH - padding * 0.62f; float separatorY = topY - GetHeaderBandHeight(currentSize) * 0.48f; Color mainLine = colWhiteGlow; mainLine.a *= Alpha * chromeReveal * 0.44f; if (textReveal > 0f) { Draw.FontSize = fontSize * 0.74f; mainLine.a *= textReveal; DrawTextWithGhost(new Vector3(leftX, topY, 0f), terminalTitle, mainLine); } Color separator = colWhiteGlow; separator.a *= Alpha * chromeReveal * textReveal * 0.26f; DrawContinuousLine( new Vector3(lineLeftX, separatorY, 0f), new Vector3(lineRightX, separatorY, 0f), separator, borderThickness * 0.34f, 0.61f ); } private void DrawDoorOutline(Vector2 currentSize, float frameProgress) { float doorReveal = 1f - Mathf.InverseLerp(0.22f, 0.72f, frameProgress); if (doorReveal <= 0f) return; float halfW = currentSize.x * 0.5f; float halfH = currentSize.y * 0.5f; float shoulderY = Mathf.Lerp(halfH - currentSize.y * 0.24f, halfH - currentSize.y * 0.12f, 1f - doorReveal); float sideX = Mathf.Lerp(halfW * 0.78f, halfW * 0.92f, 1f - doorReveal); float topX = Mathf.Lerp(halfW * 0.35f, halfW * 0.78f, 1f - doorReveal); Color c = colAmberDim; c.a *= Alpha * doorReveal * 0.72f; DrawSegmentedLine(new Vector3(-sideX, -halfH, 0f), new Vector3(-sideX, shoulderY, 0f), c, borderThickness * 0.95f, borderGapRatio * 0.55f, 0.82f); DrawSegmentedLine(new Vector3(sideX, -halfH, 0f), new Vector3(sideX, shoulderY, 0f), c, borderThickness * 0.82f, borderGapRatio * 0.7f, 0.87f); DrawSegmentedLine(new Vector3(-sideX, shoulderY, 0f), new Vector3(-topX, halfH, 0f), c, borderThickness * 0.9f, borderGapRatio * 0.45f, 0.91f); DrawSegmentedLine(new Vector3(sideX, shoulderY, 0f), new Vector3(topX, halfH, 0f), c, borderThickness * 0.78f, borderGapRatio * 0.6f, 0.96f); DrawSegmentedLine(new Vector3(-topX, halfH, 0f), new Vector3(topX, halfH, 0f), c, borderThickness * 0.88f, borderGapRatio, 1.03f); } // ----- 日志文本区 ----- private void DrawLogLines(Vector2 currentSize, float textReveal) { if (textReveal <= 0f) return; float halfW = currentSize.x * 0.5f; float halfH = currentSize.y * 0.5f; float topY = GetContentTopY(currentSize); float bottomY = -halfH + padding * 0.88f; int visibleLineBudget = Mathf.Max(1, Mathf.FloorToInt((topY - bottomY) / lineHeight)); int visibleLines = Mathf.Min(maxVisibleLines, visibleLineBudget); List rows = BuildRenderRows(); int startIdx = Mathf.Max(0, rows.Count - visibleLines); for (int i = startIdx; i < rows.Count; i++) { int row = i - startIdx; float y = topY - row * lineHeight; RenderRow renderRow = rows[i]; Color c = renderRow.color; c.a *= Alpha * textReveal; Draw.FontSize = fontSize; Vector3 textPos = new Vector3(-halfW + padding, y, 0f); string displayText = renderRow.text; if (renderRow.hasCursorSlot) { // 位图字体不支持 ,用 末两位控制透明度 string colorHex = ColorUtility.ToHtmlStringRGBA(c); string cursorHex = renderRow.showCursor ? colorHex : colorHex[..6] + "00"; displayText += $"{'\u2588'}"; } DrawTextWithGhost(textPos, displayText, c); } } // ----- 输入区 ----- private void DrawInputArea(Vector2 currentSize, float textReveal) { // 输入已并入日志流,不再单独绘制底部输入区。 } private float GetContentTopY(Vector2 currentSize) { float halfH = currentSize.y * 0.5f; return halfH - padding - GetHeaderBandHeight(currentSize) * 0.46f - 0.04f; } private float GetHeaderBandHeight(Vector2 currentSize) { return Mathf.Min(headerBandHeight, currentSize.y * 0.18f); } private float GetFooterBandHeight(Vector2 currentSize) { return Mathf.Max(inputAreaHeight, Mathf.Min(footerBandHeight, currentSize.y * 0.2f)); } private void DrawTextWithGhost(Vector3 position, string text, Color color) { if (string.IsNullOrEmpty(text)) return; Color ghost = color; ghost.a *= textGhostAlpha * 0.82f; Draw.Text(position + new Vector3(textGhostOffset.x, textGhostOffset.y, 0f), Quaternion.identity, text, TextAlign.TopLeft, ghost); Color phosphor = color; phosphor.a *= 0.18f; Draw.Text(position + new Vector3(0f, -0.01f, 0f), Quaternion.identity, text, TextAlign.TopLeft, phosphor); Draw.Text(position, Quaternion.identity, text, TextAlign.TopLeft, color); } private float EstimateTextWidth(string text) { if (string.IsNullOrEmpty(text)) return 0f; float width = 0f; foreach (char c in text) { width += c switch { ' ' => fontSize * 0.34f, '.' or ',' or ':' or ';' or '!' or '|' or '\'' => fontSize * 0.24f, '(' or ')' or '[' or ']' => fontSize * 0.36f, _ when c > 255 => fontSize * 1.02f, _ => fontSize * 0.58f }; } return width; } private List BuildRenderRows() { List rows = new(_lines.Count + 4); for (int i = 0; i < _lines.Count; i++) { rows.Add(new RenderRow { text = _lines[i], color = _lineColors.Count > i ? _lineColors[i] : colAmberMain, showCursor = false }); } if (!string.IsNullOrEmpty(_typingCurrent)) { rows.Add(new RenderRow { text = _typingCurrent, color = _typingColor, showCursor = false }); } if (_phase == TerminalPhase.WaitingInput || _phase == TerminalPhase.Confirming) { rows.Add(new RenderRow { text = GetInputDisplayText(), color = colWhiteGlow, showCursor = _phase == TerminalPhase.WaitingInput && IsCursorCurrentlyVisible(), hasCursorSlot = _phase == TerminalPhase.WaitingInput // 始终占位,保持宽度不变 }); } if (_phase == TerminalPhase.WaitingInput) { rows.Add(new RenderRow { text = ">> TYPE ANSWER | ENTER = CONFIRM", color = colAmberDim, showCursor = false }); } if (_phase == TerminalPhase.Confirming) { rows.Add(new RenderRow { text = ">> Submit? [Y/Enter = YES | N = EDIT]", color = colWhiteGlow, showCursor = false }); } return rows; } private bool IsCursorCurrentlyVisible() { if (!_cursorVisible) return false; float interval = Mathf.Max(0.08f, cursorBlinkRate); float fullCycle = interval * 2f; return Mathf.Repeat(GetVisualTime(), fullCycle) < interval; } private void CommitPendingInputToHistory(bool includeConfirmPrompt) { AddLine(GetInputDisplayText(), colWhiteGlow); if (includeConfirmPrompt) AddLine(">> Submit? [Y/Enter = YES | N = EDIT]", colWhiteGlow); } private string GetInputDisplayText() { string composition = Application.isPlaying && _phase == TerminalPhase.WaitingInput ? Input.compositionString : string.Empty; return promptString + _inputBuffer + composition; } private void DrawContinuousLine(Vector3 start, Vector3 end, Color color, float thickness, float noiseSeed) { DrawSegmentedLine(start, end, color, thickness, 0f, noiseSeed); } private void DrawSegmentedLine(Vector3 start, Vector3 end, Color color, float thickness, float gapRatio, float noiseSeed) { float flicker = GetBorderFlicker(noiseSeed); float clampedGap = Mathf.Clamp(gapRatio, 0f, 0.45f); Vector3 direction = end - start; if (clampedGap <= 0.0005f) { Color straight = color; straight.a *= flicker; Draw.Thickness = thickness; Draw.Line(start, end, straight); if (borderGhostAlpha > 0.001f && borderGhostOffset > 0f && direction.sqrMagnitude > 0.0001f) { Vector3 simpleGhostOffset = Vector3.Cross(direction.normalized, Vector3.forward) * borderGhostOffset; Color straightGhost = color; straightGhost.a *= borderGhostAlpha * flicker; Draw.Thickness = thickness * 0.45f; Draw.Line(start + simpleGhostOffset, end + simpleGhostOffset, straightGhost); } return; } Vector3 midpoint = Vector3.Lerp(start, end, 0.5f); Vector3 gapOffset = direction * (clampedGap * 0.5f); Vector3 segmentAEnd = midpoint - gapOffset; Vector3 segmentBStart = midpoint + gapOffset; Color main = color; main.a *= flicker; Draw.Thickness = thickness; Draw.Line(start, segmentAEnd, main); Draw.Line(segmentBStart, end, main); if (borderGhostAlpha <= 0.001f || borderGhostOffset <= 0f || direction.sqrMagnitude <= 0.0001f) return; Vector3 ghostOffset = Vector3.Cross(direction.normalized, Vector3.forward) * borderGhostOffset; Color ghost = color; ghost.a *= borderGhostAlpha * flicker; Draw.Thickness = thickness * 0.45f; Draw.Line(start + ghostOffset, segmentAEnd + ghostOffset, ghost); Draw.Line(segmentBStart + ghostOffset, end + ghostOffset, ghost); } private float GetBorderFlicker(float seed) { return Mathf.Lerp(1f - borderFlickerAmount, 1f, SampleNoise(seed, borderFlickerSpeed)); } private float SampleNoise(float seed, float speed) { return Mathf.PerlinNoise(seed, GetVisualTime() * speed); } private float GetVisualTime() { #if UNITY_EDITOR if (!Application.isPlaying) return (float)EditorApplication.timeSinceStartup; #endif return Time.unscaledTime; } #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; float delay = charDelay * UnityEngine.Random.Range(0.8f, 1.4f); if (c is '.' or ':' or '/' or '_' or '"' or '?') delay *= 2f; else if (c == ' ') delay *= 0.55f; yield return new WaitForSeconds(delay); } // 提交为已完成行 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; float speed = Mathf.Max(0.25f, bootSpeedMultiplier); yield return new WaitForSeconds(0.3f / speed); foreach (var entry in bootSequence) { Color c = StyleToColor(entry.style); float charDelay = typewriterCharDelay / speed; _typewriterCoroutine = StartCoroutine(TypewriterLine(entry.text, c, charDelay)); yield return WaitForTypewriter(); if (entry.delayAfter > 0f) yield return new WaitForSeconds(entry.delayAfter / speed); } yield return new WaitForSeconds(0.3f / speed); 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 = ""; _compositionWasActive = false; OverrideImeMode(IMECompositionMode.On); StartCursorBlink(); } private void HandleTypingInput() { string compositionBeforeInput = Input.compositionString; bool compositionHandledEnter = _compositionWasActive || !string.IsNullOrEmpty(compositionBeforeInput); bool enterSeen = false; bool backspaceSeen = false; foreach (char c in Input.inputString) { if (c == '\b') { backspaceSeen = true; if (_inputBuffer.Length > 0) _inputBuffer = _inputBuffer[..^1]; } else if (c == '\n' || c == '\r') { enterSeen = true; } else if (!char.IsControl(c) && _inputBuffer.Length < maxInputLength) { _inputBuffer += c; } } if (!backspaceSeen && Input.GetKeyDown(KeyCode.Backspace) && string.IsNullOrEmpty(Input.compositionString)) { if (_inputBuffer.Length > 0) _inputBuffer = _inputBuffer[..^1]; } // inputString 在部分输入法/键盘布局下收不到回车,用 GetKeyDown 兜底 if (!enterSeen && (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))) enterSeen = true; string compositionAfterInput = Input.compositionString; compositionHandledEnter |= !string.IsNullOrEmpty(compositionAfterInput); _compositionWasActive = !string.IsNullOrEmpty(compositionAfterInput); if (enterSeen && !compositionHandledEnter && _inputBuffer.Length > 0) EnterConfirmPhase(); } /// /// 保存玩家原输入法状态,并按终端当前阶段切换 IME。 /// private void OverrideImeMode(IMECompositionMode mode) { if (!_imeOverridden) { _previousImeMode = Input.imeCompositionMode; _imeOverridden = true; } Input.imeCompositionMode = mode; Input.compositionCursorPos = new Vector2(Screen.width * 0.5f, Screen.height * 0.62f); } private void RestoreImeMode() { if (!_imeOverridden) return; Input.imeCompositionMode = _previousImeMode; _imeOverridden = false; _compositionWasActive = false; } #endregion #region Confirm Phase private void EnterConfirmPhase() { _phase = TerminalPhase.Confirming; OverrideImeMode(IMECompositionMode.Off); _compositionWasActive = false; StopCursorBlink(); } private void HandleConfirmInput() { if (Input.GetKeyDown(KeyCode.Y) || Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)) { CommitPendingInputToHistory(includeConfirmPrompt: true); _phase = TerminalPhase.EndAnimation; StartCoroutine(HandleSubmission(_inputBuffer)); } else if (Input.GetKeyDown(KeyCode.N)) { _phase = TerminalPhase.WaitingInput; OverrideImeMode(IMECompositionMode.On); StartCursorBlink(); } } #endregion #region End Animation private IEnumerator HandleSubmission(string input) { RestoreImeMode(); string val = input.Trim().ToLowerInvariant(); 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); DreamDoorSystem.Instance?.HideAll(); } 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; } private void StopCursorBlink() { _cursorVisible = false; } #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 } private struct RenderRow { public string text; public Color color; public bool showCursor; public bool hasCursorSlot; // 为 true 时始终追加光标占位符,避免基线浮动 } #endregion private void EditorApply() { #if UNITY_EDITOR ApplyEditorPreviewState(); if (previewInEditor) SceneView.RepaintAll(); else DrawCommand.ClearAllCommands(); #endif } #if UNITY_EDITOR private void OnEditorSceneGui(SceneView sceneView) { if (Application.isPlaying || !previewInEditor || Alpha <= 0.01f) return; DrawEditorScenePreview(); } private void DrawEditorScenePreview() { float frameProgress = Mathf.SmoothStep(0f, 1f, MorphProgress); float textReveal = frameProgress >= 0.999f ? 1f : 0f; Vector2 currentSize = Vector2.Lerp(morphStartSize, terminalSize, frameProgress); Matrix4x4 previousMatrix = Handles.matrix; Color previousColor = Handles.color; CompareFunction previousZTest = Handles.zTest; Handles.matrix = transform.localToWorldMatrix; Handles.zTest = CompareFunction.Always; Color bg = colBg; bg.a *= Alpha; Handles.DrawSolidRectangleWithOutline( new Rect(-currentSize.x * 0.5f, -currentSize.y * 0.5f, currentSize.x, currentSize.y), bg, Color.clear); float halfW = currentSize.x * 0.5f - padding * 0.48f; float halfH = currentSize.y * 0.5f - padding * 0.42f; Color border = colWhiteGlow; border.a *= Alpha * 0.58f; Handles.color = border; Handles.DrawLine(new Vector3(-halfW, halfH, 0f), new Vector3(halfW, halfH, 0f)); Handles.DrawLine(new Vector3(-halfW, -halfH, 0f), new Vector3(halfW, -halfH, 0f)); Handles.DrawLine(new Vector3(-halfW, -halfH, 0f), new Vector3(-halfW, halfH, 0f)); Handles.DrawLine(new Vector3(halfW, -halfH, 0f), new Vector3(halfW, halfH, 0f)); if (textReveal <= 0f) { Handles.matrix = previousMatrix; Handles.color = previousColor; Handles.zTest = previousZTest; return; } float contentHalfW = currentSize.x * 0.5f; float contentHalfH = currentSize.y * 0.5f; float titleY = contentHalfH - padding * 0.62f; float separatorY = titleY - GetHeaderBandHeight(currentSize) * 0.48f; DrawEditorLabel(new Vector3(-contentHalfW + padding * 0.9f, titleY, 0f), terminalTitle, colWhiteGlow, fontSize * 0.74f); Color separator = colWhiteGlow; separator.a *= Alpha * textReveal * 0.26f; Handles.color = separator; Handles.DrawLine( new Vector3(-halfW, separatorY, 0f), new Vector3(halfW, separatorY, 0f)); float topY = GetContentTopY(currentSize); float bottomY = -contentHalfH + padding * 0.88f; int visibleLineBudget = Mathf.Max(1, Mathf.FloorToInt((topY - bottomY) / lineHeight)); int visibleLines = Mathf.Min(maxVisibleLines, visibleLineBudget); List rows = BuildRenderRows(); int startIdx = Mathf.Max(0, rows.Count - visibleLines); for (int i = startIdx; i < rows.Count; i++) { int row = i - startIdx; float y = topY - row * lineHeight; RenderRow renderRow = rows[i]; Color c = renderRow.color; c.a *= Alpha * textReveal; string displayText = renderRow.text; if (renderRow.hasCursorSlot) displayText += renderRow.showCursor ? "\u2588" : " "; DrawEditorLabel(new Vector3(-contentHalfW + padding, y, 0f), displayText, c, fontSize); } Handles.matrix = previousMatrix; Handles.color = previousColor; Handles.zTest = previousZTest; } private void DrawEditorLabel(Vector3 localPosition, string text, Color color, float worldFontSize) { if (string.IsNullOrEmpty(text)) return; if (_editorPreviewLabelStyle == null) { _editorPreviewLabelStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.UpperLeft, richText = false }; if (terminalFont != null && terminalFont.sourceFontFile != null) _editorPreviewLabelStyle.font = terminalFont.sourceFontFile; } _editorPreviewLabelStyle.fontSize = Mathf.Clamp(Mathf.RoundToInt(worldFontSize * 12f), 8, 28); _editorPreviewLabelStyle.normal.textColor = color; Handles.Label(localPosition, text, _editorPreviewLabelStyle); } private void EditorPreviewUpdate() { if (Application.isPlaying || !previewInEditor || !NeedsAnimatedEditorRepaint()) return; double now = EditorApplication.timeSinceStartup; float interval = Mathf.Max(0.08f, cursorBlinkRate); if (now - _editorLastRepaintTime < interval) return; _editorLastRepaintTime = now; SceneView.RepaintAll(); } private bool NeedsAnimatedEditorRepaint() { return animatePreviewCursor && previewStage == PreviewStage.WaitingInput; } private void ApplyEditorPreviewState() { if (Application.isPlaying) return; if (!previewInEditor) { Alpha = 0f; MorphProgress = 0f; _phase = TerminalPhase.Idle; _lines.Clear(); _lineColors.Clear(); _typingCurrent = ""; _inputBuffer = ""; _cursorVisible = false; DrawCommand.ClearAllCommands(); 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; break; case PreviewStage.Confirming: _phase = TerminalPhase.Confirming; _cursorVisible = false; break; } } private void AddPreviewLine(string text) { Color color = text.Contains("WARNING") || text.Contains("FALSE") || text.Contains("ERROR") ? colAlert : text.Contains("SYSTEM") || text.Contains("LOADING") ? colAmberDim : colAmberMain; _lines.Add(text); _lineColors.Add(color); } #endif private void EnsureDefaultFontReference() { #if UNITY_EDITOR if (terminalFont == null) { const string fontPath = "Assets/Font/Assets/WenQuanYi Bitmap Song 14px SDF.asset"; terminalFont = AssetDatabase.LoadAssetAtPath(fontPath); } #endif } } }