1006 lines
35 KiB
C#
1006 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 = ">> 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;
|
|
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;
|
|
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;
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|