Files
StoryGame_Unity/Assets/02_Scripts/_UI/DialogMarkup.cs
2026-08-09 13:58:32 +09:00

116 lines
4.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using UnityEngine;
// 대사 텍스트의 저작용 단축 표기를 리치 텍스트로 펼친다.
//
// 그날 밤 [[피 묻은 칼]]을 봤습니다
// ↓
// 그날 밤 <link=glow><color=#FF3B30>피 묻은 칼</color></link>을 봤습니다
//
// 왜 단축 표기를 두는가: 그래프의 Talk Text 칸에 <color=#FF3B30>을 손으로 쓰면
// 오타가 조용히 깨지고, 색을 바꿀 때 모든 대사를 찾아 고쳐야 한다.
// 색은 TypewriterStyle.EmphasisColor 한 곳에서 관리된다.
//
// <link>을 같이 붙이는 이유: TextGlow가 Glyph.linkID로 강조 글자를 식별해 맥동시킨다.
public static class DialogMarkup
{
public const string OPEN = "[[";
public const string CLOSE = "]]";
// 저작자가 원하는 자리에 직접 뜸을 넣는 표기: 그날 밤<<delay:0.4>> 저는 봤습니다
//
// 문장부호 자동 쉼(TypewriterStyle.CommaPause/SentencePause)은 "어디에나 일정하게" 걸리는
// 바닥값이고, 이건 "여기서 이만큼" 을 콕 집는 수단이다. 둘은 대체 관계가 아니라 층이 다르다.
public const string DELAY_OPEN = "<<delay:";
public const string DELAY_CLOSE = ">>";
// <<delay:x>> 표기를 텍스트에서 걷어내고, 걷어낸 자리(결과 문자열 기준 인덱스)와 초를 모아 준다.
//
// 왜 텍스트에서 반드시 제거해야 하는가: Typewriter는 받은 문자열을 그대로 Label에 넣으므로,
// 표기가 남아 있으면 화면에 "<<delay:0.4>>"가 그대로 찍힌다.
// 인덱스를 결과 문자열 기준으로 잡는 것도 같은 이유다 — Typewriter의 커서와 같은 좌표계여야 한다.
//
// delays는 인덱스 오름차순으로 채워진다 (앞에서부터 훑으므로 자연히 정렬된다).
public static string ExtractDelays(string text, List<(int Index, float Seconds)> delays)
{
delays?.Clear();
if (string.IsNullOrEmpty(text) || text.IndexOf(DELAY_OPEN, StringComparison.Ordinal) < 0)
return text;
var sb = new StringBuilder(text.Length);
int i = 0;
while (i < text.Length)
{
int open = text.IndexOf(DELAY_OPEN, i, StringComparison.Ordinal);
if (open < 0)
{
sb.Append(text, i, text.Length - i);
break;
}
int close = text.IndexOf(DELAY_CLOSE, open + DELAY_OPEN.Length, StringComparison.Ordinal);
if (close < 0)
{
sb.Append(text, i, text.Length - i); // 닫히지 않은 표기 — 손대지 않고 그대로 둔다
break;
}
string raw = text.Substring(open + DELAY_OPEN.Length, close - open - DELAY_OPEN.Length);
// 숫자가 아니면 저작 실수다. 조용히 지우면 "왜 뜸이 없지"를 한참 찾게 되므로,
// 경고를 남기고 표기를 화면에 그대로 남겨 눈에 띄게 한다.
if (!float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float seconds))
{
Debug.LogWarning($"[DialogMarkup] 지연 표기의 숫자를 읽지 못함: {DELAY_OPEN}{raw}{DELAY_CLOSE}");
sb.Append(text, i, close + DELAY_CLOSE.Length - i);
i = close + DELAY_CLOSE.Length;
continue;
}
sb.Append(text, i, open - i);
delays?.Add((sb.Length, Mathf.Max(0f, seconds)));
i = close + DELAY_CLOSE.Length;
}
return sb.ToString();
}
public static string Expand(string text, TypewriterStyle style)
{
if (string.IsNullOrEmpty(text) || text.IndexOf(OPEN, StringComparison.Ordinal) < 0)
return text;
string hex = ColorUtility.ToHtmlStringRGB(style != null ? style.EmphasisColor : Color.red);
var sb = new StringBuilder(text.Length + 48);
int i = 0;
while (i < text.Length)
{
int open = text.IndexOf(OPEN, i, StringComparison.Ordinal);
if (open < 0)
{
sb.Append(text, i, text.Length - i);
break;
}
int close = text.IndexOf(CLOSE, open + OPEN.Length, StringComparison.Ordinal);
if (close < 0)
{
sb.Append(text, i, text.Length - i); // 닫히지 않은 표기 — 손대지 않고 그대로 둔다
break;
}
sb.Append(text, i, open - i);
sb.Append("<link=").Append(TextGlow.LINK_ID).Append("><color=#").Append(hex).Append('>');
sb.Append(text, open + OPEN.Length, close - open - OPEN.Length);
sb.Append("</color></link>");
i = close + CLOSE.Length;
}
return sb.ToString();
}
}