Files
aibis-dream/Assets/Scripts/UI/Components/DreamTerminalUI.cs
T

1009 lines
35 KiB
C#

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 = "root@sys:~$ ./boot --test", style = LogStyle.Dim, delayAfter = 0.2f },
new BootLogEntry { text = "[ OK ] Test passed.", style = LogStyle.Normal, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ render --target ocean && aplay sea.wav", style = LogStyle.Dim, delayAfter = 0.2f },
new BootLogEntry { text = "[ OK ] Ocean rendered. Audio stream started.", style = LogStyle.Normal, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ render --stop", style = LogStyle.Dim, delayAfter = 0.2f },
new BootLogEntry { text = "[ OK ] Render pipeline closed.", style = LogStyle.Normal, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ showcode", style = LogStyle.Alert, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ print_string(\"海是存在的吗?\")", style = LogStyle.Highlight, delayAfter = 0.2f },
};
#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;
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()
{
EnsureDefaultFontReference();
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();
EnsureDefaultFontReference();
if (!Application.isPlaying)
ApplyEditorPreviewState();
}
private void OnValidate()
{
EnsureDefaultFontReference();
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;
}
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;
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 = 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;
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
#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;
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
}
}
}