1213 lines
43 KiB
C#
1213 lines
43 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using Shapes;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
#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:~$ dream.render --stop --session 0742", style = LogStyle.Dim, delayAfter = 0.15f },
|
||
new BootLogEntry { text = "[ OK ] Dream renderer stopped.", style = LogStyle.Normal, delayAfter = 0.25f },
|
||
new BootLogEntry { text = "[ RAW ] Untitled_Greybox_0 exposed.", style = LogStyle.Normal, delayAfter = 0.35f },
|
||
new BootLogEntry { text = "root@sys:~$ trace --tail /log/dream_0742.rec", style = LogStyle.Dim, delayAfter = 0.15f },
|
||
new BootLogEntry { text = "[0005] audio.play(\"sea_ambient\", source=door.rear)", style = LogStyle.Dim, delayAfter = 0.1f },
|
||
new BootLogEntry { text = "[0006] memory.project(\"glass_sea\")", style = LogStyle.Dim, delayAfter = 0.1f },
|
||
new BootLogEntry { text = "[ WARN ] memory source unresolved; overflow detected.", style = LogStyle.Alert, delayAfter = 0.4f },
|
||
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;
|
||
private IMECompositionMode _previousImeMode = IMECompositionMode.Auto;
|
||
private bool _imeOverridden;
|
||
private bool _compositionWasActive;
|
||
|
||
// 全局 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()
|
||
{
|
||
#if UNITY_EDITOR
|
||
SceneView.duringSceneGui -= OnEditorSceneGui;
|
||
EditorApplication.update -= EditorPreviewUpdate;
|
||
if (!Application.isPlaying)
|
||
{
|
||
DrawCommand.ClearAllCommands();
|
||
_phase = TerminalPhase.Idle;
|
||
StopAllCoroutines();
|
||
return;
|
||
}
|
||
#endif
|
||
base.OnDisable();
|
||
_phase = TerminalPhase.Idle;
|
||
RestoreImeMode();
|
||
StopAllCoroutines();
|
||
}
|
||
|
||
public override void OnEnable()
|
||
{
|
||
EnsureDefaultFontReference();
|
||
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
{
|
||
SceneView.duringSceneGui -= OnEditorSceneGui;
|
||
SceneView.duringSceneGui += OnEditorSceneGui;
|
||
EditorApplication.update -= EditorPreviewUpdate;
|
||
EditorApplication.update += EditorPreviewUpdate;
|
||
EditorApply();
|
||
return;
|
||
}
|
||
#endif
|
||
base.OnEnable();
|
||
}
|
||
|
||
private void OnValidate()
|
||
{
|
||
EnsureDefaultFontReference();
|
||
MorphProgress = morphProgress;
|
||
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
EditorApply();
|
||
#endif
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 公共接口
|
||
|
||
public void Begin()
|
||
{
|
||
if (_phase != TerminalPhase.Idle) return;
|
||
_lines.Clear();
|
||
_lineColors.Clear();
|
||
_inputBuffer = "";
|
||
MorphProgress = 1f;
|
||
StartCoroutine(RunBootSequence());
|
||
}
|
||
|
||
public void ForceEnd()
|
||
{
|
||
StopAllCoroutines();
|
||
RestoreImeMode();
|
||
_phase = TerminalPhase.Done;
|
||
}
|
||
|
||
public void ResetVisualState()
|
||
{
|
||
StopAllCoroutines();
|
||
RestoreImeMode();
|
||
_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;
|
||
EditorApply();
|
||
}
|
||
|
||
[ContextMenu("Preview/Waiting Input")]
|
||
private void PreviewWaitingInput()
|
||
{
|
||
previewInEditor = true;
|
||
previewStage = PreviewStage.WaitingInput;
|
||
previewAlpha = 1f;
|
||
previewMorphProgress = 1f;
|
||
EditorApply();
|
||
}
|
||
|
||
[ContextMenu("Preview/Clear")]
|
||
private void ClearPreview()
|
||
{
|
||
previewInEditor = false;
|
||
EditorApply();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Shapes 绘制(DrawShapes 覆写)
|
||
|
||
public override void DrawShapes(Camera cam)
|
||
{
|
||
#if UNITY_EDITOR
|
||
// 编辑模式预览改走 SceneView Handles,URP 下 Shapes 文本池不会回收。
|
||
if (!Application.isPlaying)
|
||
return;
|
||
#endif
|
||
|
||
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)
|
||
{
|
||
rows.Add(new RenderRow
|
||
{
|
||
text = ">> TYPE ANSWER | ENTER = CONFIRM",
|
||
color = colAmberDim,
|
||
showCursor = false
|
||
});
|
||
}
|
||
|
||
if (_phase == TerminalPhase.Confirming)
|
||
{
|
||
rows.Add(new RenderRow
|
||
{
|
||
text = ">> Submit? [Y/Enter = YES | N = EDIT]",
|
||
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/Enter = YES | N = EDIT]", colWhiteGlow);
|
||
}
|
||
|
||
private string GetInputDisplayText()
|
||
{
|
||
string composition = Application.isPlaying && _phase == TerminalPhase.WaitingInput
|
||
? Input.compositionString
|
||
: string.Empty;
|
||
return promptString + _inputBuffer + composition;
|
||
}
|
||
|
||
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 = "";
|
||
_compositionWasActive = false;
|
||
OverrideImeMode(IMECompositionMode.On);
|
||
StartCursorBlink();
|
||
}
|
||
|
||
private void HandleTypingInput()
|
||
{
|
||
string compositionBeforeInput = Input.compositionString;
|
||
bool compositionHandledEnter = _compositionWasActive || !string.IsNullOrEmpty(compositionBeforeInput);
|
||
bool enterSeen = false;
|
||
bool backspaceSeen = false;
|
||
|
||
foreach (char c in Input.inputString)
|
||
{
|
||
if (c == '\b')
|
||
{
|
||
backspaceSeen = true;
|
||
if (_inputBuffer.Length > 0)
|
||
_inputBuffer = _inputBuffer[..^1];
|
||
}
|
||
else if (c == '\n' || c == '\r')
|
||
{
|
||
enterSeen = true;
|
||
}
|
||
else if (!char.IsControl(c) && _inputBuffer.Length < maxInputLength)
|
||
{
|
||
_inputBuffer += c;
|
||
}
|
||
}
|
||
|
||
if (!backspaceSeen && Input.GetKeyDown(KeyCode.Backspace) && string.IsNullOrEmpty(Input.compositionString))
|
||
{
|
||
if (_inputBuffer.Length > 0)
|
||
_inputBuffer = _inputBuffer[..^1];
|
||
}
|
||
|
||
// inputString 在部分输入法/键盘布局下收不到回车,用 GetKeyDown 兜底
|
||
if (!enterSeen && (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)))
|
||
enterSeen = true;
|
||
|
||
string compositionAfterInput = Input.compositionString;
|
||
compositionHandledEnter |= !string.IsNullOrEmpty(compositionAfterInput);
|
||
_compositionWasActive = !string.IsNullOrEmpty(compositionAfterInput);
|
||
|
||
if (enterSeen && !compositionHandledEnter && _inputBuffer.Length > 0)
|
||
EnterConfirmPhase();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存玩家原输入法状态,并按终端当前阶段切换 IME。
|
||
/// </summary>
|
||
private void OverrideImeMode(IMECompositionMode mode)
|
||
{
|
||
if (!_imeOverridden)
|
||
{
|
||
_previousImeMode = Input.imeCompositionMode;
|
||
_imeOverridden = true;
|
||
}
|
||
|
||
Input.imeCompositionMode = mode;
|
||
Input.compositionCursorPos = new Vector2(Screen.width * 0.5f, Screen.height * 0.62f);
|
||
}
|
||
|
||
private void RestoreImeMode()
|
||
{
|
||
if (!_imeOverridden) return;
|
||
Input.imeCompositionMode = _previousImeMode;
|
||
_imeOverridden = false;
|
||
_compositionWasActive = false;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Confirm Phase
|
||
|
||
private void EnterConfirmPhase()
|
||
{
|
||
_phase = TerminalPhase.Confirming;
|
||
OverrideImeMode(IMECompositionMode.Off);
|
||
_compositionWasActive = false;
|
||
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;
|
||
OverrideImeMode(IMECompositionMode.On);
|
||
StartCursorBlink();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region End Animation
|
||
|
||
private IEnumerator HandleSubmission(string input)
|
||
{
|
||
RestoreImeMode();
|
||
string val = input.Trim().ToLowerInvariant();
|
||
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
|
||
|
||
private void EditorApply()
|
||
{
|
||
#if UNITY_EDITOR
|
||
ApplyEditorPreviewState();
|
||
|
||
if (previewInEditor)
|
||
SceneView.RepaintAll();
|
||
else
|
||
DrawCommand.ClearAllCommands();
|
||
#endif
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
private void OnEditorSceneGui(SceneView sceneView)
|
||
{
|
||
if (Application.isPlaying || !previewInEditor || Alpha <= 0.01f)
|
||
return;
|
||
|
||
DrawEditorScenePreview();
|
||
}
|
||
|
||
private void DrawEditorScenePreview()
|
||
{
|
||
float frameProgress = Mathf.SmoothStep(0f, 1f, MorphProgress);
|
||
float textReveal = frameProgress >= 0.999f ? 1f : 0f;
|
||
Vector2 currentSize = Vector2.Lerp(morphStartSize, terminalSize, frameProgress);
|
||
|
||
Matrix4x4 previousMatrix = Handles.matrix;
|
||
Color previousColor = Handles.color;
|
||
CompareFunction previousZTest = Handles.zTest;
|
||
|
||
Handles.matrix = transform.localToWorldMatrix;
|
||
Handles.zTest = CompareFunction.Always;
|
||
|
||
Color bg = colBg;
|
||
bg.a *= Alpha;
|
||
Handles.DrawSolidRectangleWithOutline(
|
||
new Rect(-currentSize.x * 0.5f, -currentSize.y * 0.5f, currentSize.x, currentSize.y),
|
||
bg,
|
||
Color.clear);
|
||
|
||
float halfW = currentSize.x * 0.5f - padding * 0.48f;
|
||
float halfH = currentSize.y * 0.5f - padding * 0.42f;
|
||
Color border = colWhiteGlow;
|
||
border.a *= Alpha * 0.58f;
|
||
Handles.color = border;
|
||
Handles.DrawLine(new Vector3(-halfW, halfH, 0f), new Vector3(halfW, halfH, 0f));
|
||
Handles.DrawLine(new Vector3(-halfW, -halfH, 0f), new Vector3(halfW, -halfH, 0f));
|
||
Handles.DrawLine(new Vector3(-halfW, -halfH, 0f), new Vector3(-halfW, halfH, 0f));
|
||
Handles.DrawLine(new Vector3(halfW, -halfH, 0f), new Vector3(halfW, halfH, 0f));
|
||
|
||
if (textReveal <= 0f)
|
||
{
|
||
Handles.matrix = previousMatrix;
|
||
Handles.color = previousColor;
|
||
Handles.zTest = previousZTest;
|
||
return;
|
||
}
|
||
|
||
float contentHalfW = currentSize.x * 0.5f;
|
||
float contentHalfH = currentSize.y * 0.5f;
|
||
float titleY = contentHalfH - padding * 0.62f;
|
||
float separatorY = titleY - GetHeaderBandHeight(currentSize) * 0.48f;
|
||
|
||
DrawEditorLabel(new Vector3(-contentHalfW + padding * 0.9f, titleY, 0f), terminalTitle, colWhiteGlow, fontSize * 0.74f);
|
||
|
||
Color separator = colWhiteGlow;
|
||
separator.a *= Alpha * textReveal * 0.26f;
|
||
Handles.color = separator;
|
||
Handles.DrawLine(
|
||
new Vector3(-halfW, separatorY, 0f),
|
||
new Vector3(halfW, separatorY, 0f));
|
||
|
||
float topY = GetContentTopY(currentSize);
|
||
float bottomY = -contentHalfH + 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;
|
||
|
||
string displayText = renderRow.text;
|
||
if (renderRow.hasCursorSlot)
|
||
displayText += renderRow.showCursor ? "\u2588" : " ";
|
||
|
||
DrawEditorLabel(new Vector3(-contentHalfW + padding, y, 0f), displayText, c, fontSize);
|
||
}
|
||
|
||
Handles.matrix = previousMatrix;
|
||
Handles.color = previousColor;
|
||
Handles.zTest = previousZTest;
|
||
}
|
||
|
||
private void DrawEditorLabel(Vector3 localPosition, string text, Color color, float worldFontSize)
|
||
{
|
||
if (string.IsNullOrEmpty(text))
|
||
return;
|
||
|
||
if (_editorPreviewLabelStyle == null)
|
||
{
|
||
_editorPreviewLabelStyle = new GUIStyle(EditorStyles.label)
|
||
{
|
||
alignment = TextAnchor.UpperLeft,
|
||
richText = false
|
||
};
|
||
if (terminalFont != null && terminalFont.sourceFontFile != null)
|
||
_editorPreviewLabelStyle.font = terminalFont.sourceFontFile;
|
||
}
|
||
|
||
_editorPreviewLabelStyle.fontSize = Mathf.Clamp(Mathf.RoundToInt(worldFontSize * 12f), 8, 28);
|
||
_editorPreviewLabelStyle.normal.textColor = color;
|
||
Handles.Label(localPosition, text, _editorPreviewLabelStyle);
|
||
}
|
||
|
||
private void EditorPreviewUpdate()
|
||
{
|
||
if (Application.isPlaying || !previewInEditor || !NeedsAnimatedEditorRepaint())
|
||
return;
|
||
|
||
double now = EditorApplication.timeSinceStartup;
|
||
float interval = Mathf.Max(0.08f, cursorBlinkRate);
|
||
if (now - _editorLastRepaintTime < interval)
|
||
return;
|
||
|
||
_editorLastRepaintTime = now;
|
||
SceneView.RepaintAll();
|
||
}
|
||
|
||
private bool NeedsAnimatedEditorRepaint()
|
||
{
|
||
return animatePreviewCursor && previewStage == PreviewStage.WaitingInput;
|
||
}
|
||
|
||
private void ApplyEditorPreviewState()
|
||
{
|
||
if (Application.isPlaying)
|
||
return;
|
||
|
||
if (!previewInEditor)
|
||
{
|
||
Alpha = 0f;
|
||
MorphProgress = 0f;
|
||
_phase = TerminalPhase.Idle;
|
||
_lines.Clear();
|
||
_lineColors.Clear();
|
||
_typingCurrent = "";
|
||
_inputBuffer = "";
|
||
_cursorVisible = false;
|
||
DrawCommand.ClearAllCommands();
|
||
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
|
||
}
|
||
}
|
||
}
|