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