Files
aibis-dream/Assets/Scripts/UI/Components/DreamTerminalUI.cs
T
2026-03-06 18:09:15 +08:00

757 lines
25 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 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.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<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 _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
}
}