Files
aibis-dream/Assets/Scripts/UI/Components/DreamTerminalUI.cs
T
2026-04-02 12:05:16 +08:00

974 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using AibisDream;
using DG.Tweening;
using Shapes;
using TMPro;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace AibisDream.UI
{
/// <summary>
/// 梦境命令窗口:全部用 Shapes 即时模式绘制,无需手动配置任何 Canvas 或 UI 组件。
/// 挂在 Door 节点(或其子节点)上即可。
/// </summary>
[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<BootLogEntry> 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<string> _lines = new();
private readonly List<Color> _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;
// 全局 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()
{
base.OnDisable();
_phase = TerminalPhase.Idle;
StopAllCoroutines();
}
public override void OnEnable()
{
base.OnEnable();
EnsureDefaultFontReference();
#if UNITY_EDITOR
if (!Application.isPlaying)
EditorRunPreviewTick();
#endif
}
private void OnValidate()
{
EnsureDefaultFontReference();
MorphProgress = morphProgress;
}
#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;
}
public Vector2 EvaluateFrameSize(float progress)
{
float frameProgress = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(progress));
return Vector2.Lerp(morphStartSize, terminalSize, frameProgress);
}
#endregion
#region Shapes 绘制(DrawShapes 覆写)
public override void DrawShapes(Camera cam)
{
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<RenderRow> 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)
{
// 位图字体不支持 <alpha>,用 <color=#RRGGBBAA> 末两位控制透明度
string colorHex = ColorUtility.ToHtmlStringRGBA(c);
string cursorHex = renderRow.showCursor
? colorHex
: colorHex[..6] + "00";
displayText += $"<color=#{cursorHex}>{'\u2588'}</color>";
}
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<RenderRow> BuildRenderRows()
{
List<RenderRow> 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 && _inputBuffer.Length > 0)
{
rows.Add(new RenderRow
{
text = ">> Press Enter to submit",
color = colAmberDim,
showCursor = false
});
}
if (_phase == TerminalPhase.Confirming)
{
rows.Add(new RenderRow
{
text = ">> Submit? (Y/N)",
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/N)", colWhiteGlow);
}
private string GetInputDisplayText()
{
return promptString + _inputBuffer;
}
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 = "";
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')
{
EnterConfirmPhase();
return;
}
else if (!char.IsControl(c) && _inputBuffer.Length < maxInputLength)
{
_inputBuffer += c;
}
}
}
#endregion
#region Confirm Phase
private void EnterConfirmPhase()
{
_phase = TerminalPhase.Confirming;
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;
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;
DreamDoorSystem.Instance?.HideAll();
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;
}
private void StopCursorBlink()
{
_cursorVisible = false;
}
#endregion
#region 数据结构
[Serializable]
public class BootLogEntry
{
public string text;
public LogStyle style;
public float delayAfter;
}
public 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
#if UNITY_EDITOR
private double _editorLastRepaintTime;
/// <summary>仅编辑器:由 DreamTerminalUIEditorCustomEditor)在需要时调用。</summary>
public void EditorRunPreviewTick()
{
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;
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<TMP_FontAsset>(fontPath);
}
#endif
}
}
}