feat: 火山玩法本地化
仍有硬编码残留,待处理
This commit is contained in:
@@ -17,6 +17,7 @@ namespace AibisDream
|
||||
public const string FixWhackMoleData = Root + "/维修·打地鼠/关卡数据";
|
||||
public const string HuoShanEmotionWave = Root + "/火山/情绪波配置";
|
||||
public const string HuoShanWaveform = Root + "/火山/波形配置";
|
||||
public const string HuoShanExpressionContent = Root + "/火山/表达内容配置";
|
||||
public const string BlockPuzzleValidator = Root + "/方块拼图/验证器";
|
||||
public const string MemoryPunchTapeCatalog = Root + "/记忆/打孔带目录";
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ namespace AibisDream.Utility
|
||||
|
||||
public const string UITextTable = "UIText";
|
||||
public const string ChapterInfoTable = "ChapterInfo";
|
||||
public const string ParamsTable = "Params";
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AibisDream.Framework;
|
||||
using AibisDream.Utility;
|
||||
using UnityEngine;
|
||||
@@ -79,10 +80,16 @@ namespace AibisDream
|
||||
return LocalizedLine.InvalidLine;
|
||||
}
|
||||
|
||||
// 对 substitutions 做本地化解析(保留旧 LocalisedLineProvider 的能力)
|
||||
for (int i = 0; i < line.Substitutions.Length; i++)
|
||||
// 对 substitutions 并行做本地化解析(保留旧 LocalisedLineProvider 的能力)。
|
||||
if (line.Substitutions.Length > 0)
|
||||
{
|
||||
line.Substitutions[i] = LocalizationKit.LocalizeParam(line.Substitutions[i]);
|
||||
var localizationTasks = new Task<string>[line.Substitutions.Length];
|
||||
for (int i = 0; i < line.Substitutions.Length; i++)
|
||||
localizationTasks[i] = LocalizationKit.LocalizeParamAsync(line.Substitutions[i]);
|
||||
|
||||
string[] localizedSubstitutions = await Task.WhenAll(localizationTasks);
|
||||
for (int i = 0; i < localizedSubstitutions.Length; i++)
|
||||
line.Substitutions[i] = localizedSubstitutions[i];
|
||||
}
|
||||
|
||||
// 先展开项目级语义标记,再保留旧 LocalisedLineProvider 的文本转义处理。
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace AibisDream.FixSystem
|
||||
|
||||
private PunchTape punchTapeData;
|
||||
private PunchTapeDefinition punchTapeDefinition;
|
||||
private int localizationRefreshVersion;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
@@ -34,6 +35,7 @@ namespace AibisDream.FixSystem
|
||||
private void OnDisable()
|
||||
{
|
||||
LocalizationSettings.SelectedLocaleChanged -= HandleSelectedLocaleChanged;
|
||||
localizationRefreshVersion++;
|
||||
}
|
||||
|
||||
public void Setup(PunchTape punchTape, PunchTapeDefinition definition)
|
||||
@@ -57,8 +59,7 @@ namespace AibisDream.FixSystem
|
||||
|
||||
if (nameText != null)
|
||||
{
|
||||
RefreshLocalizedName(LocalizationKit.GetLocaleKind(
|
||||
LocalizationSettings.SelectedLocale?.Identifier.Code));
|
||||
RefreshLocalizedName(LocalizationSettings.SelectedLocale);
|
||||
}
|
||||
|
||||
memoryIndexLabelText?.RefreshString();
|
||||
@@ -73,17 +74,26 @@ namespace AibisDream.FixSystem
|
||||
|
||||
private void HandleSelectedLocaleChanged(Locale locale)
|
||||
{
|
||||
RefreshLocalizedName(LocalizationKit.GetLocaleKind(locale?.Identifier.Code));
|
||||
RefreshLocalizedName(locale);
|
||||
}
|
||||
|
||||
private void RefreshLocalizedName(LocaleKind localeKind)
|
||||
private async void RefreshLocalizedName(Locale locale)
|
||||
{
|
||||
if (nameText == null || punchTapeDefinition == null)
|
||||
return;
|
||||
|
||||
int version = ++localizationRefreshVersion;
|
||||
string localizedName = await LocalizationKit.LocalizeParamAsync(
|
||||
punchTapeDefinition.DisplayNameParam,
|
||||
locale);
|
||||
if (version != localizationRefreshVersion ||
|
||||
nameText == null ||
|
||||
punchTapeDefinition == null)
|
||||
return;
|
||||
|
||||
nameText.StringReference.Arguments = new object[]
|
||||
{
|
||||
LocalizationKit.LocalizeParam(punchTapeDefinition.DisplayNameParam, localeKind)
|
||||
localizedName
|
||||
};
|
||||
nameText.RefreshString();
|
||||
}
|
||||
|
||||
@@ -13,10 +13,8 @@ namespace AibisDream.Kit
|
||||
public class ConfigUtil : SingletonBase<ConfigUtil>
|
||||
{
|
||||
private const string CharacterConfigPath = "/Config/character.csv";
|
||||
private const string ParamConfigPath = "/Config/params.csv";
|
||||
|
||||
private Dictionary<string, Character> _characters;
|
||||
private Dictionary<string, Param> _params;
|
||||
|
||||
/// <summary>
|
||||
/// 用于单例模式
|
||||
@@ -41,7 +39,6 @@ namespace AibisDream.Kit
|
||||
public override void OnSingletonInit()
|
||||
{
|
||||
InitCharacterConfig();
|
||||
InitParamConfig();
|
||||
}
|
||||
|
||||
private void InitCharacterConfig()
|
||||
@@ -52,15 +49,6 @@ namespace AibisDream.Kit
|
||||
.ToDictionary(item => item.Key, item => item);
|
||||
}
|
||||
|
||||
private void InitParamConfig()
|
||||
{
|
||||
var paramList = CsvUtil.ReadAsBean<Param>(Application.streamingAssetsPath + ParamConfigPath);
|
||||
_params = paramList
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item.Key))
|
||||
.ToDictionary(item => item.Key, item => item);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 按key获取角色预设
|
||||
/// </summary>
|
||||
@@ -72,17 +60,6 @@ namespace AibisDream.Kit
|
||||
return _characters.TryGetValue(key, out character);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按key获取参数本地化配置
|
||||
/// </summary>
|
||||
/// <param name="key">key</param>
|
||||
/// <param name="param">参数配置</param>
|
||||
/// <returns>是否能获取</returns>
|
||||
public bool TryGetParam(string key, out Param param)
|
||||
{
|
||||
return _params.TryGetValue(key, out param);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按类型读取Json配置
|
||||
/// </summary>
|
||||
@@ -176,4 +153,4 @@ namespace AibisDream.Kit
|
||||
return fileNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using AibisDream.Framework;
|
||||
|
||||
namespace AibisDream.Kit
|
||||
{
|
||||
public class Param
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Cn { get; set; }
|
||||
public string En { get; set; }
|
||||
public string Ja { get; set; }
|
||||
|
||||
public string LocalizeByKind(LocaleKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case LocaleKind.En:
|
||||
return string.IsNullOrWhiteSpace(En) ? Key : En;
|
||||
case LocaleKind.Ja:
|
||||
if (!string.IsNullOrWhiteSpace(Ja)) return Ja;
|
||||
return string.IsNullOrWhiteSpace(Cn) ? Key : Cn;
|
||||
case LocaleKind.Cn:
|
||||
default:
|
||||
return string.IsNullOrWhiteSpace(Cn) ? Key : Cn;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections;
|
||||
using AibisDream.Kit;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AibisDream.Utility;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Settings;
|
||||
@@ -127,39 +129,85 @@ namespace AibisDream.Framework
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地化参数
|
||||
/// 判断字符串是否为 l10n 参数引用。
|
||||
/// </summary>
|
||||
/// <param name="param">参数名</param>
|
||||
/// <returns></returns>
|
||||
public static string LocalizeParam(string param)
|
||||
public static bool IsLocalizedParam(string value)
|
||||
{
|
||||
return LocalizeParam(param, CurrentLocaleKind);
|
||||
return !string.IsNullOrEmpty(value) &&
|
||||
value.StartsWith(LocalizationPrefix, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按指定语言列解析本地化参数,供需要响应 Locale 切换的非 Yarn UI 使用。
|
||||
/// 从 Params String Table 异步解析 l10n 参数。非 l10n 参数原样返回。
|
||||
/// 当前 Locale 条目为空时保留空值,不使用任何 Locale 回退。
|
||||
/// </summary>
|
||||
public static string LocalizeParam(string param, LocaleKind localeKind)
|
||||
public static async Task<string> LocalizeParamAsync(string param, Locale locale = null)
|
||||
{
|
||||
// 非本地化参数直接返回
|
||||
if (string.IsNullOrEmpty(param)) return param;
|
||||
if (!param.StartsWith(LocalizationPrefix)) return param;
|
||||
if (!IsLocalizedParam(param))
|
||||
return param;
|
||||
|
||||
var localizationKey = param[LocalizationPrefix.Length..];
|
||||
if (ConfigUtil.Instance.TryGetParam(localizationKey, out var value))
|
||||
string localizationKey = GetL10NParamKey(param);
|
||||
if (string.IsNullOrEmpty(localizationKey))
|
||||
{
|
||||
return value.LocalizeByKind(localeKind);
|
||||
Debug.LogError("[LocalizationKit] l10n 参数缺少 Entry Key。");
|
||||
return "⟦empty⟧";
|
||||
}
|
||||
|
||||
// 如果找不到,则使用原始值
|
||||
Debug.LogWarning($"Variable Key '{localizationKey}' not found in params csv.");
|
||||
return localizationKey;
|
||||
try
|
||||
{
|
||||
await LocalizationSettings.InitializationOperation.Task;
|
||||
Locale resolvedLocale = locale != null ? locale : LocalizationSettings.SelectedLocale;
|
||||
var operation = LocalizationSettings.StringDatabase.GetTableEntryAsync(
|
||||
ConstRef.ParamsTable,
|
||||
localizationKey,
|
||||
resolvedLocale,
|
||||
FallbackBehavior.DontUseFallback);
|
||||
var result = await operation.Task;
|
||||
if (result.Entry != null)
|
||||
return result.Entry.LocalizedValue ?? string.Empty;
|
||||
|
||||
string localeCode = resolvedLocale != null
|
||||
? resolvedLocale.Identifier.Code
|
||||
: "<null>";
|
||||
Debug.LogError(
|
||||
$"[LocalizationKit] Params 缺少条目:Table={ConstRef.ParamsTable}, " +
|
||||
$"Key={localizationKey}, Locale={localeCode}。");
|
||||
return $"⟦{localizationKey}⟧";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
string localeCode = locale != null
|
||||
? locale.Identifier.Code
|
||||
: LocalizationSettings.SelectedLocale?.Identifier.Code ?? "<null>";
|
||||
Debug.LogError(
|
||||
$"[LocalizationKit] Params 查询失败:Table={ConstRef.ParamsTable}, " +
|
||||
$"Key={localizationKey}, Locale={localeCode}。\n{exception}");
|
||||
return $"⟦{localizationKey}⟧";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 协程版 l10n 参数解析,供 Yarn Command 和 MonoBehaviour 使用。
|
||||
/// </summary>
|
||||
public static IEnumerator LocalizeParam(
|
||||
string param,
|
||||
Locale locale,
|
||||
Action<string> completed)
|
||||
{
|
||||
Task<string> task = LocalizeParamAsync(param, locale);
|
||||
while (!task.IsCompleted)
|
||||
yield return null;
|
||||
|
||||
string result = task.Status == TaskStatus.RanToCompletion
|
||||
? task.Result
|
||||
: $"⟦{GetL10NParamKey(param)}⟧";
|
||||
completed?.Invoke(result);
|
||||
}
|
||||
|
||||
public static string GetL10NParamKey(string key)
|
||||
{
|
||||
// 本地化参数则去除前缀,非本地化参数直接返回
|
||||
return key.StartsWith(LocalizationPrefix) ? key[LocalizationPrefix.Length..] : key;
|
||||
return IsLocalizedParam(key) ? key[LocalizationPrefix.Length..] : key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AibisDream.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class ExpressionRoundDefinition
|
||||
{
|
||||
[SerializeField] private string id;
|
||||
[SerializeField] private string targetReference;
|
||||
[SerializeField] private string tokenReference;
|
||||
[SerializeField, Min(0)] private int nonTargetParticleCount;
|
||||
|
||||
public string Id => id;
|
||||
public string TargetReference => targetReference;
|
||||
public string TokenReference => tokenReference;
|
||||
public int NonTargetParticleCount => nonTargetParticleCount;
|
||||
|
||||
public bool TryValidate(out string error)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
error = "轮次 ID 不能为空。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LocalizationKit.IsLocalizedParam(targetReference))
|
||||
{
|
||||
error = $"轮次 '{id}' 的目标文字必须配置为 l10n.* 引用。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LocalizationKit.IsLocalizedParam(tokenReference))
|
||||
{
|
||||
error = $"轮次 '{id}' 的干扰 token 必须配置为 l10n.* 引用。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nonTargetParticleCount < 0)
|
||||
{
|
||||
error = $"轮次 '{id}' 的非目标粒子数量不能为负数。";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[CreateAssetMenu(
|
||||
fileName = "ExpressionContentCatalog",
|
||||
menuName = AibisAssetMenus.HuoShanExpressionContent)]
|
||||
public sealed class ExpressionContentCatalog : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string actorLoopReference;
|
||||
[SerializeField] private List<ExpressionRoundDefinition> rounds = new();
|
||||
|
||||
private Dictionary<string, ExpressionRoundDefinition> roundById;
|
||||
|
||||
public string ActorLoopReference => actorLoopReference;
|
||||
public IReadOnlyList<ExpressionRoundDefinition> Rounds => rounds;
|
||||
|
||||
public bool TryGetRound(string roundId, out ExpressionRoundDefinition definition)
|
||||
{
|
||||
EnsureLookup();
|
||||
if (string.IsNullOrWhiteSpace(roundId))
|
||||
{
|
||||
definition = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return roundById.TryGetValue(roundId, out definition);
|
||||
}
|
||||
|
||||
public List<string> GetValidationErrors()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
if (!LocalizationKit.IsLocalizedParam(actorLoopReference))
|
||||
errors.Add("Actor Loop 必须配置为 l10n.* 引用。");
|
||||
|
||||
var ids = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (rounds == null || rounds.Count == 0)
|
||||
{
|
||||
errors.Add("Catalog 中没有表达轮次。");
|
||||
return errors;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rounds.Count; i++)
|
||||
{
|
||||
ExpressionRoundDefinition round = rounds[i];
|
||||
if (round == null)
|
||||
{
|
||||
errors.Add($"rounds[{i}] 为空。");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!round.TryValidate(out string error))
|
||||
errors.Add(error);
|
||||
if (!string.IsNullOrWhiteSpace(round.Id) && !ids.Add(round.Id))
|
||||
errors.Add($"存在重复轮次 ID:'{round.Id}'。");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private void EnsureLookup()
|
||||
{
|
||||
if (roundById != null)
|
||||
return;
|
||||
|
||||
roundById = new Dictionary<string, ExpressionRoundDefinition>(StringComparer.Ordinal);
|
||||
if (rounds == null)
|
||||
return;
|
||||
|
||||
foreach (ExpressionRoundDefinition round in rounds)
|
||||
{
|
||||
if (round == null || string.IsNullOrWhiteSpace(round.Id) || roundById.ContainsKey(round.Id))
|
||||
continue;
|
||||
roundById.Add(round.Id, round);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
roundById = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c12e0d2c486f4ea6a5d4f70878b6e7c1
|
||||
guid: a86a72005fba4c99a59962fb9e92df83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -4,6 +4,7 @@ using UnityEngine;
|
||||
using AibisDream.FixSystem;
|
||||
using AibisDream.MiniGame.Language;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Localization;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
@@ -17,6 +18,7 @@ namespace AibisDream
|
||||
[SerializeField] private GameObject expressionView;
|
||||
[SerializeField] private LogReleasePresentationController logReleasePresentation;
|
||||
[SerializeField] private ExpressionScreenPresentationController screenPresentation;
|
||||
[SerializeField] private ExpressionContentCatalog contentCatalog;
|
||||
|
||||
[Header("默认配置")]
|
||||
[SerializeField] private List<string> defaultAnxietyPhrases = new List<string>
|
||||
@@ -54,6 +56,18 @@ namespace AibisDream
|
||||
[SerializeField, Range(0.3f, 1f)] private float screenStressCap = 0.85f;
|
||||
|
||||
public LogReleasePresentationController LogReleasePresentation => logReleasePresentation;
|
||||
public ExpressionContentCatalog ContentCatalog => contentCatalog;
|
||||
public Locale ActiveExpressionLocale { get; private set; }
|
||||
|
||||
public Locale GetExpressionLocale(Locale fallback)
|
||||
{
|
||||
return ActiveExpressionLocale != null ? ActiveExpressionLocale : fallback;
|
||||
}
|
||||
|
||||
public void SetActiveExpressionLocale(Locale locale)
|
||||
{
|
||||
ActiveExpressionLocale = locale;
|
||||
}
|
||||
|
||||
/// <summary>当前屏幕 Glitch 档位(0–1),由 expression_screen_glitch 持续保持。</summary>
|
||||
public float ScreenGlitchLevel => screenGlitchIntensity;
|
||||
@@ -274,6 +288,7 @@ namespace AibisDream
|
||||
/// </summary>
|
||||
public void CloseView()
|
||||
{
|
||||
ActiveExpressionLocale = null;
|
||||
screenPresentation?.ResetImmediate();
|
||||
ResetScreenGlitchState();
|
||||
ApplyScreenGlitchProperties();
|
||||
@@ -941,6 +956,7 @@ namespace AibisDream
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
ActiveExpressionLocale = null;
|
||||
ResetScreenGlitchState();
|
||||
ApplyScreenGlitchProperties();
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
/// <summary>
|
||||
/// 将玩家可见文字按 Unicode 文本元素拆分。空白保留在布局中,但不生成粒子。
|
||||
/// 随机粒子字符池不使用本类,维持现有行为。
|
||||
/// </summary>
|
||||
public static class ExpressionTextTokenizer
|
||||
{
|
||||
public readonly struct Layout
|
||||
{
|
||||
public Layout(List<string> visibleElements, List<float> offsets)
|
||||
{
|
||||
VisibleElements = visibleElements;
|
||||
Offsets = offsets;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> VisibleElements { get; }
|
||||
public IReadOnlyList<float> Offsets { get; }
|
||||
public int Count => VisibleElements?.Count ?? 0;
|
||||
}
|
||||
|
||||
public static List<string> GetVisibleElements(string text)
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return result;
|
||||
|
||||
TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
string element = enumerator.GetTextElement();
|
||||
if (!string.IsNullOrWhiteSpace(element))
|
||||
result.Add(element);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Layout BuildLayout(
|
||||
string text,
|
||||
float characterSpacing,
|
||||
float whitespaceSpacingMultiplier)
|
||||
{
|
||||
var visible = new List<string>();
|
||||
var positions = new List<float>();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return new Layout(visible, positions);
|
||||
|
||||
float spacing = characterSpacing > 0f ? characterSpacing : 1f;
|
||||
float whitespaceAdvance = spacing * whitespaceSpacingMultiplier;
|
||||
float cursor = 0f;
|
||||
TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
string element = enumerator.GetTextElement();
|
||||
if (string.IsNullOrWhiteSpace(element))
|
||||
{
|
||||
if (visible.Count > 0)
|
||||
cursor += whitespaceAdvance;
|
||||
continue;
|
||||
}
|
||||
|
||||
visible.Add(element);
|
||||
positions.Add(cursor);
|
||||
cursor += spacing;
|
||||
}
|
||||
|
||||
if (positions.Count > 0)
|
||||
{
|
||||
float center = (positions[0] + positions[positions.Count - 1]) * 0.5f;
|
||||
for (int i = 0; i < positions.Count; i++)
|
||||
positions[i] -= center;
|
||||
}
|
||||
|
||||
return new Layout(visible, positions);
|
||||
}
|
||||
|
||||
public static int FindVisibleSequence(
|
||||
IReadOnlyList<string> source,
|
||||
IReadOnlyList<string> fragment)
|
||||
{
|
||||
if (source == null || fragment == null || fragment.Count == 0 || fragment.Count > source.Count)
|
||||
return -1;
|
||||
|
||||
int lastStart = source.Count - fragment.Count;
|
||||
for (int start = 0; start <= lastStart; start++)
|
||||
{
|
||||
bool matches = true;
|
||||
for (int i = 0; i < fragment.Count; i++)
|
||||
{
|
||||
if (!string.Equals(source[start + i], fragment[i], System.StringComparison.Ordinal))
|
||||
{
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches)
|
||||
return start;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8ba4ca26b9b46649c0da7161dc9c756
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -93,6 +93,7 @@ namespace AibisDream.MiniGame.Language
|
||||
[SerializeField] private float floatingTextFontSize = 3f; // 背景浮动文字大小
|
||||
[SerializeField] private bool targetParticleBold = true; // 目标粒子是否加粗
|
||||
[SerializeField] private bool nonTargetParticleBold = false; // 非目标粒子是否加粗
|
||||
[SerializeField, Range(0.1f, 2f)] private float whitespaceSpacingMultiplier = 0.6f;
|
||||
|
||||
[Header("非目标粒子干扰效果")]
|
||||
[SerializeField, Range(0f, 0.1f)] private float nonTargetShakeIntensity = 0.02f; // 非目标粒子抖动强度
|
||||
@@ -354,10 +355,18 @@ namespace AibisDream.MiniGame.Language
|
||||
CaptureDefaultCandidateCountFromInspector();
|
||||
|
||||
string useSentence = string.IsNullOrEmpty(sentence) ? "这是一个测试示例" : sentence;
|
||||
int visibleTargetCount = ExpressionTextTokenizer.GetVisibleElements(useSentence).Count;
|
||||
if (visibleTargetCount == 0)
|
||||
{
|
||||
Debug.LogError("[LanguageParticleManager] 目标句没有可显示的 Unicode 文本元素;初始化已中止。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (nonTargetParticleTotal >= 0)
|
||||
{
|
||||
candidateCount = Mathf.Max(useSentence.Length + nonTargetParticleTotal, useSentence.Length);
|
||||
candidateCount = Mathf.Max(
|
||||
visibleTargetCount + nonTargetParticleTotal,
|
||||
visibleTargetCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -409,7 +418,7 @@ namespace AibisDream.MiniGame.Language
|
||||
TextParticle.SetAnxietyPhrases(anxietyPhrases);
|
||||
|
||||
// 根据目标句子更新目标粒子数量
|
||||
redParticleCount = Mathf.Min(targetSentence.Length, candidateCount);
|
||||
redParticleCount = Mathf.Min(visibleTargetCount, candidateCount);
|
||||
|
||||
// 重新初始化游戏状态并播放入场动画
|
||||
ResetGame(true);
|
||||
@@ -1247,7 +1256,11 @@ namespace AibisDream.MiniGame.Language
|
||||
SetStatusUIVisibility(true, 0f);
|
||||
|
||||
orderedRedParticles = GetOrderedRedParticles();
|
||||
int displayLength = Mathf.Min(orderedRedParticles.Count, targetSentence.Length);
|
||||
ExpressionTextTokenizer.Layout layout = ExpressionTextTokenizer.BuildLayout(
|
||||
targetSentence,
|
||||
0.9f,
|
||||
whitespaceSpacingMultiplier);
|
||||
int displayLength = Mathf.Min(orderedRedParticles.Count, layout.Count);
|
||||
|
||||
if (displayLength < orderedRedParticles.Count)
|
||||
{
|
||||
@@ -1256,20 +1269,17 @@ namespace AibisDream.MiniGame.Language
|
||||
|
||||
for (int i = 0; i < displayLength; i++)
|
||||
{
|
||||
finalTargetCharacters.Add(targetSentence[i].ToString());
|
||||
finalTargetCharacters.Add(layout.VisibleElements[i]);
|
||||
}
|
||||
|
||||
Vector3 displayCenter = worldCanvas != null ? worldCanvas.transform.position : Vector3.zero;
|
||||
if (finalTargetCharacters.Count > 0)
|
||||
{
|
||||
float spacing = 0.9f;
|
||||
float totalWidth = spacing * Mathf.Max(0, finalTargetCharacters.Count - 1);
|
||||
float startX = -totalWidth / 2f;
|
||||
finalTargetPositions.Clear();
|
||||
Bounds clip = GetCompletionClipBounds();
|
||||
for (int i = 0; i < finalTargetCharacters.Count; i++)
|
||||
{
|
||||
Vector3 pos = displayCenter + new Vector3(startX + i * spacing, 0f, 0f);
|
||||
Vector3 pos = displayCenter + new Vector3(layout.Offsets[i], 0f, 0f);
|
||||
finalTargetPositions.Add(ClampPositionToBounds(pos, clip));
|
||||
}
|
||||
}
|
||||
@@ -2151,6 +2161,7 @@ namespace AibisDream.MiniGame.Language
|
||||
List<CandidateParticle> particles = GetPresentationPhraseParticles();
|
||||
if (!ValidatePresentationPhrase(text, particles, "真话失稳"))
|
||||
yield break;
|
||||
List<string> characters = ExpressionTextTokenizer.GetVisibleElements(text);
|
||||
|
||||
Color unstableColor = truthAttackColor;
|
||||
if (!string.IsNullOrWhiteSpace(colorHex))
|
||||
@@ -2169,8 +2180,8 @@ namespace AibisDream.MiniGame.Language
|
||||
float totalDuration = Mathf.Max(0.12f, duration);
|
||||
float resolvedShakeAmplitude = Mathf.Max(0f, shakeAmplitude);
|
||||
float resolvedShakeSpeed = Mathf.Max(0f, shakeSpeed);
|
||||
List<Vector3> destinations = CalculatePresentationPhrasePositions(text.Length);
|
||||
UpdateFinalPresentationPhrase(text, destinations);
|
||||
List<Vector3> destinations = CalculatePresentationPhrasePositions(text);
|
||||
UpdateFinalPresentationPhrase(characters, destinations);
|
||||
|
||||
for (int i = 0; i < particles.Count; i++)
|
||||
{
|
||||
@@ -2197,13 +2208,13 @@ namespace AibisDream.MiniGame.Language
|
||||
particle.resolvedColorProgress = 1f;
|
||||
particle.calmProgress = 1f;
|
||||
|
||||
if (i >= text.Length)
|
||||
if (i >= characters.Count)
|
||||
{
|
||||
particle.alpha = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
particle.ForceSetCharacter(text[i].ToString());
|
||||
particle.ForceSetCharacter(characters[i]);
|
||||
particle.alpha = 1f;
|
||||
particle.SetFocusInterference(
|
||||
unstableColor,
|
||||
@@ -2221,7 +2232,7 @@ namespace AibisDream.MiniGame.Language
|
||||
|
||||
yield return new WaitForSeconds(totalDuration);
|
||||
|
||||
for (int i = 0; i < text.Length && i < particles.Count; i++)
|
||||
for (int i = 0; i < characters.Count && i < particles.Count; i++)
|
||||
{
|
||||
CandidateParticle particle = particles[i];
|
||||
if (particle == null)
|
||||
@@ -2239,6 +2250,7 @@ namespace AibisDream.MiniGame.Language
|
||||
List<CandidateParticle> particles = GetPresentationPhraseParticles();
|
||||
if (!ValidatePresentationPhrase(text, particles, "真话攻击"))
|
||||
yield break;
|
||||
List<string> characters = ExpressionTextTokenizer.GetVisibleElements(text);
|
||||
|
||||
PreparePresentationPhraseControl();
|
||||
|
||||
@@ -2247,8 +2259,8 @@ namespace AibisDream.MiniGame.Language
|
||||
float travelDuration = totalDuration * 0.68f;
|
||||
float settleDuration = Mathf.Max(0.04f, totalDuration - travelDuration);
|
||||
int directionSign = truthAttackSequenceIndex++ % 2 == 0 ? -1 : 1;
|
||||
List<Vector3> destinations = CalculatePresentationPhrasePositions(text.Length);
|
||||
UpdateFinalPresentationPhrase(text, destinations);
|
||||
List<Vector3> destinations = CalculatePresentationPhrasePositions(text);
|
||||
UpdateFinalPresentationPhrase(characters, destinations);
|
||||
|
||||
for (int i = 0; i < particles.Count; i++)
|
||||
{
|
||||
@@ -2276,7 +2288,7 @@ namespace AibisDream.MiniGame.Language
|
||||
particle.resolvedColorProgress = 1f;
|
||||
particle.calmProgress = 1f;
|
||||
|
||||
if (i >= text.Length)
|
||||
if (i >= characters.Count)
|
||||
{
|
||||
DOTween.To(
|
||||
() => particle.alpha,
|
||||
@@ -2288,7 +2300,7 @@ namespace AibisDream.MiniGame.Language
|
||||
continue;
|
||||
}
|
||||
|
||||
particle.ForceSetCharacter(text[i].ToString());
|
||||
particle.ForceSetCharacter(characters[i]);
|
||||
particle.alpha = 1f;
|
||||
particle.focusInterferenceProgress = 1f;
|
||||
|
||||
@@ -2325,7 +2337,7 @@ namespace AibisDream.MiniGame.Language
|
||||
|
||||
yield return new WaitForSeconds(totalDuration);
|
||||
|
||||
for (int i = 0; i < text.Length && i < particles.Count; i++)
|
||||
for (int i = 0; i < characters.Count && i < particles.Count; i++)
|
||||
{
|
||||
CandidateParticle particle = particles[i];
|
||||
if (particle == null)
|
||||
@@ -2344,6 +2356,7 @@ namespace AibisDream.MiniGame.Language
|
||||
List<CandidateParticle> particles = GetPresentationPhraseParticles();
|
||||
if (!ValidatePresentationPhrase(text, particles, "真话稳定"))
|
||||
yield break;
|
||||
List<string> characters = ExpressionTextTokenizer.GetVisibleElements(text);
|
||||
|
||||
PreparePresentationPhraseControl();
|
||||
|
||||
@@ -2384,8 +2397,8 @@ namespace AibisDream.MiniGame.Language
|
||||
|
||||
yield return new WaitForSeconds(breakDuration);
|
||||
|
||||
List<Vector3> destinations = CalculatePresentationPhrasePositions(text.Length);
|
||||
UpdateFinalPresentationPhrase(text, destinations);
|
||||
List<Vector3> destinations = CalculatePresentationPhrasePositions(text);
|
||||
UpdateFinalPresentationPhrase(characters, destinations);
|
||||
for (int i = 0; i < particles.Count; i++)
|
||||
{
|
||||
CandidateParticle particle = particles[i];
|
||||
@@ -2410,14 +2423,14 @@ namespace AibisDream.MiniGame.Language
|
||||
particle.resolvedColorProgress = 1f;
|
||||
particle.calmProgress = 1f;
|
||||
|
||||
if (i >= text.Length)
|
||||
if (i >= characters.Count)
|
||||
{
|
||||
particle.alpha = 0f;
|
||||
particle.ClearFocusInterference();
|
||||
continue;
|
||||
}
|
||||
|
||||
particle.ForceSetCharacter(text[i].ToString());
|
||||
particle.ForceSetCharacter(characters[i]);
|
||||
particle.SetFocusInterference(truthAttackColor, 0f, 0f);
|
||||
particle.focusInterferenceProgress = 1f;
|
||||
particle.alpha = 0.15f;
|
||||
@@ -2453,8 +2466,8 @@ namespace AibisDream.MiniGame.Language
|
||||
if (particle == null)
|
||||
continue;
|
||||
particle.ClearFocusInterference();
|
||||
particle.alpha = i < text.Length ? 1f : 0f;
|
||||
if (i < text.Length)
|
||||
particle.alpha = i < characters.Count ? 1f : 0f;
|
||||
if (i < characters.Count)
|
||||
{
|
||||
particle.transform.position = destinations[i];
|
||||
particle.transform.localScale = Vector3.one * focusZoomScale;
|
||||
@@ -2494,10 +2507,17 @@ namespace AibisDream.MiniGame.Language
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.Length > particles.Count)
|
||||
int requiredParticles = ExpressionTextTokenizer.GetVisibleElements(text).Count;
|
||||
if (requiredParticles == 0)
|
||||
{
|
||||
Debug.LogWarning($"[LanguageParticleManager] {context}没有可显示的文本元素;已安全跳过。");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requiredParticles > particles.Count)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LanguageParticleManager] {context}“{text}”需要 {text.Length} 个目标粒子," +
|
||||
$"[LanguageParticleManager] {context}“{text}”需要 {requiredParticles} 个目标粒子," +
|
||||
$"当前只有 {particles.Count} 个;为避免截断已跳过。");
|
||||
return false;
|
||||
}
|
||||
@@ -2521,27 +2541,32 @@ namespace AibisDream.MiniGame.Language
|
||||
connectionRenderer.TargetAlphaScale = 0f;
|
||||
}
|
||||
|
||||
private List<Vector3> CalculatePresentationPhrasePositions(int characterCount)
|
||||
private List<Vector3> CalculatePresentationPhrasePositions(string text)
|
||||
{
|
||||
var positions = new List<Vector3>(Mathf.Max(0, characterCount));
|
||||
Vector3 center = worldCanvas != null ? worldCanvas.transform.position : transform.position;
|
||||
float spacing = Mathf.Max(0.1f, truthAttackCharacterSpacing);
|
||||
float startX = -spacing * Mathf.Max(0, characterCount - 1) * 0.5f;
|
||||
ExpressionTextTokenizer.Layout layout = ExpressionTextTokenizer.BuildLayout(
|
||||
text,
|
||||
spacing,
|
||||
whitespaceSpacingMultiplier);
|
||||
var positions = new List<Vector3>(layout.Count);
|
||||
Vector3 center = worldCanvas != null ? worldCanvas.transform.position : transform.position;
|
||||
Bounds clip = GetCompletionClipBounds();
|
||||
for (int i = 0; i < characterCount; i++)
|
||||
for (int i = 0; i < layout.Count; i++)
|
||||
{
|
||||
Vector3 position = center + new Vector3(startX + i * spacing, 0f, 0f);
|
||||
Vector3 position = center + new Vector3(layout.Offsets[i], 0f, 0f);
|
||||
positions.Add(ClampPositionToBounds(position, clip));
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
private void UpdateFinalPresentationPhrase(string text, List<Vector3> positions)
|
||||
private void UpdateFinalPresentationPhrase(
|
||||
IReadOnlyList<string> characters,
|
||||
List<Vector3> positions)
|
||||
{
|
||||
finalTargetCharacters.Clear();
|
||||
finalTargetPositions.Clear();
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
finalTargetCharacters.Add(text[i].ToString());
|
||||
for (int i = 0; i < characters.Count; i++)
|
||||
finalTargetCharacters.Add(characters[i]);
|
||||
finalTargetPositions.AddRange(positions);
|
||||
}
|
||||
|
||||
@@ -2579,7 +2604,9 @@ namespace AibisDream.MiniGame.Language
|
||||
if (string.IsNullOrEmpty(fragment) || orderedRedParticles.Count == 0)
|
||||
yield break;
|
||||
|
||||
int startIndex = targetSentence.IndexOf(fragment, System.StringComparison.Ordinal);
|
||||
List<string> targetElements = ExpressionTextTokenizer.GetVisibleElements(targetSentence);
|
||||
List<string> fragmentElements = ExpressionTextTokenizer.GetVisibleElements(fragment);
|
||||
int startIndex = ExpressionTextTokenizer.FindVisibleSequence(targetElements, fragmentElements);
|
||||
if (startIndex < 0)
|
||||
{
|
||||
Debug.LogWarning($"[LanguageParticleManager] 真话泄漏片段“{fragment}”不在目标句“{targetSentence}”中。");
|
||||
@@ -2587,12 +2614,12 @@ namespace AibisDream.MiniGame.Language
|
||||
}
|
||||
|
||||
var leaked = new List<CandidateParticle>();
|
||||
int endExclusive = Mathf.Min(startIndex + fragment.Length, orderedRedParticles.Count);
|
||||
int endExclusive = Mathf.Min(startIndex + fragmentElements.Count, orderedRedParticles.Count);
|
||||
for (int i = startIndex; i < endExclusive; i++)
|
||||
{
|
||||
CandidateParticle particle = orderedRedParticles[i];
|
||||
if (particle == null) continue;
|
||||
particle.ForceSetCharacter(targetSentence[i].ToString());
|
||||
particle.ForceSetCharacter(targetElements[i]);
|
||||
particle.isStatic = true;
|
||||
particle.alpha = 1f;
|
||||
leaked.Add(particle);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using AibisDream.FixSystem;
|
||||
using AibisDream;
|
||||
using AibisDream.Framework;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream.MiniGame.Language
|
||||
@@ -13,6 +17,66 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
private static AibisDream.ExpressionManager ExpressionManager => FixSystemCenter.SystemDic.Get<AibisDream.ExpressionManager>();
|
||||
|
||||
private static Locale GetCommandLocale()
|
||||
{
|
||||
Locale current = LocalizationSettings.SelectedLocale;
|
||||
return ExpressionManager != null
|
||||
? ExpressionManager.GetExpressionLocale(current)
|
||||
: current;
|
||||
}
|
||||
|
||||
private static IEnumerator ResolveTextReferences(
|
||||
Locale locale,
|
||||
System.Action<string[]> completed,
|
||||
params string[] references)
|
||||
{
|
||||
var tasks = new Task<string>[references.Length];
|
||||
for (int i = 0; i < references.Length; i++)
|
||||
tasks[i] = LocalizationKit.LocalizeParamAsync(references[i], locale);
|
||||
|
||||
Task<string[]> all = Task.WhenAll(tasks);
|
||||
while (!all.IsCompleted)
|
||||
yield return null;
|
||||
|
||||
completed?.Invoke(all.Status == TaskStatus.RanToCompletion
|
||||
? all.Result
|
||||
: System.Array.Empty<string>());
|
||||
}
|
||||
|
||||
private static bool ValidateRequiredText(string value, string command, string parameter)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return true;
|
||||
|
||||
UnityEngine.Debug.LogError(
|
||||
$"[LanguageYarnCommand] {command} 的玩家可见参数 {parameter} 解析为空;命令已安全结束。");
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryParseExpressionTokens(string value, out List<string> tokens)
|
||||
{
|
||||
tokens = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
if (value.Contains("|"))
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
"[LanguageYarnCommand] 干扰 token 使用了全角分隔符“|”;请改用半角“|”。");
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] parts = value.Split('|');
|
||||
foreach (string part in parts)
|
||||
{
|
||||
string token = part.Trim();
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
tokens.Add(token);
|
||||
}
|
||||
|
||||
return tokens.Count > 0;
|
||||
}
|
||||
|
||||
private static bool TryGetPresentation(out LogReleasePresentationController presentation)
|
||||
{
|
||||
presentation = ExpressionManager != null
|
||||
@@ -30,8 +94,8 @@ namespace AibisDream.MiniGame.Language
|
||||
/// 启动表达粒子系统(在打开视图后调用),等粒子字入场完成即可返回(可交互);panel 动效并行不阻塞。
|
||||
/// 流程:若有上一轮则先淡出字与连线 → 再淡入火山释放 log → 再启动新一轮并等粒子就绪。
|
||||
/// 注意:Yarn Spinner 2.x 对同名 <see cref="YarnCommand"/> 重载支持不可靠,只保留一个注册入口,用可选参数区分 3/4 参调用。
|
||||
/// <<start_expression "哈哈|嘿嘿|呵呵" "我很害怕" "ExpressionCompleted">>
|
||||
/// <<start_expression "哈哈|嘿嘿|呵呵" "我很害怕" "ExpressionCompleted" 24>>
|
||||
/// <<start_expression "l10n.hs.exp.log1.tokens" "l10n.hs.exp.log1.target" "ExpressionCompleted">>
|
||||
/// <<start_expression "l10n.hs.exp.log1.tokens" "l10n.hs.exp.log1.target" "ExpressionCompleted" 24>>
|
||||
/// (第四项为「非目标」候选粒子总数,候选池 = 目标句字数 + 该值;不写第四项则用 Inspector 的 Candidate Count)
|
||||
/// </summary>
|
||||
/// <param name="anxietyPhrasesStr">干扰短句(笑话等),用 | 分隔,用于非目标粒子字符池,如:"哈哈|嘿嘿|呵呵"</param>
|
||||
@@ -60,21 +124,103 @@ namespace AibisDream.MiniGame.Language
|
||||
yield break;
|
||||
}
|
||||
|
||||
string[] phrases = null;
|
||||
if (!string.IsNullOrEmpty(anxietyPhrasesStr))
|
||||
Locale locale = LocalizationSettings.SelectedLocale;
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
locale,
|
||||
values => localized = values,
|
||||
anxietyPhrasesStr,
|
||||
targetSentence);
|
||||
if (localized == null || localized.Length != 2 ||
|
||||
!ValidateRequiredText(localized[0], "start_expression", nameof(anxietyPhrasesStr)) ||
|
||||
!ValidateRequiredText(localized[1], "start_expression", nameof(targetSentence)))
|
||||
yield break;
|
||||
|
||||
if (!TryParseExpressionTokens(localized[0], out List<string> phrases))
|
||||
{
|
||||
phrases = anxietyPhrasesStr.Split('|');
|
||||
for (int i = 0; i < phrases.Length; i++)
|
||||
{
|
||||
phrases[i] = phrases[i].Trim();
|
||||
}
|
||||
UnityEngine.Debug.LogError(
|
||||
"[LanguageYarnCommand] start_expression 没有有效的干扰 token;命令已安全结束。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 先清理上一轮演出,再淡出旧文字/连线并恢复 Log 操作界面。
|
||||
yield return ExpressionManager.PreparePresentationForNextRound();
|
||||
yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f);
|
||||
yield return ExpressionManager.EnsureReleaseLogFadedIn(1f);
|
||||
ExpressionManager.StartSystem(phrases, targetSentence, completionNode, nonTargetParticleTotal);
|
||||
ExpressionManager.SetActiveExpressionLocale(locale);
|
||||
ExpressionManager.StartSystem(
|
||||
phrases,
|
||||
localized[1],
|
||||
completionNode,
|
||||
nonTargetParticleTotal);
|
||||
yield return ExpressionManager.WaitUntilExpressionFlowReady();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 ExpressionContentCatalog 中的轮次配置启动表达系统。
|
||||
/// <<start_expression_round "log1" "LOG1梳理完成">>
|
||||
/// </summary>
|
||||
[YarnCommand("start_expression_round")]
|
||||
public static IEnumerator StartExpressionRound(string roundId, string completionNode = "")
|
||||
{
|
||||
if (ExpressionManager == null)
|
||||
{
|
||||
UnityEngine.Debug.LogError("ExpressionManager 未找到!请确保场景中有 ExpressionManager 组件。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
ExpressionContentCatalog catalog = ExpressionManager.ContentCatalog;
|
||||
if (catalog == null)
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
"[LanguageYarnCommand] ExpressionContentCatalog 未配置;无法启动表达轮次。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
List<string> catalogErrors = catalog.GetValidationErrors();
|
||||
if (catalogErrors.Count > 0)
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
"[LanguageYarnCommand] ExpressionContentCatalog 数据无效;无法启动表达轮次。\n" +
|
||||
string.Join("\n", catalogErrors));
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!catalog.TryGetRound(roundId, out ExpressionRoundDefinition round))
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
$"[LanguageYarnCommand] ExpressionContentCatalog 中不存在轮次 '{roundId}'。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
Locale locale = LocalizationSettings.SelectedLocale;
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
locale,
|
||||
values => localized = values,
|
||||
round.TokenReference,
|
||||
round.TargetReference);
|
||||
if (localized == null || localized.Length != 2 ||
|
||||
!ValidateRequiredText(localized[0], "start_expression_round", nameof(round.TokenReference)) ||
|
||||
!ValidateRequiredText(localized[1], "start_expression_round", nameof(round.TargetReference)))
|
||||
yield break;
|
||||
|
||||
if (!TryParseExpressionTokens(localized[0], out List<string> phrases))
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
$"[LanguageYarnCommand] 表达轮次 '{roundId}' 没有有效的干扰 token。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return ExpressionManager.PreparePresentationForNextRound();
|
||||
yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f);
|
||||
yield return ExpressionManager.EnsureReleaseLogFadedIn(1f);
|
||||
ExpressionManager.SetActiveExpressionLocale(locale);
|
||||
ExpressionManager.StartSystem(
|
||||
phrases,
|
||||
localized[1],
|
||||
completionNode ?? string.Empty,
|
||||
round.NonTargetParticleCount);
|
||||
yield return ExpressionManager.WaitUntilExpressionFlowReady();
|
||||
}
|
||||
|
||||
@@ -241,8 +387,17 @@ namespace AibisDream.MiniGame.Language
|
||||
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
|
||||
yield break;
|
||||
}
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
keyword);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_lie_begin", nameof(keyword)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
ExpressionManager.BeginExpressionLie(keyword, preset));
|
||||
ExpressionManager.BeginExpressionLie(localized[0], preset));
|
||||
}
|
||||
|
||||
[YarnCommand("expression_lie_break")]
|
||||
@@ -385,24 +540,37 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <summary>
|
||||
/// 结尾真假快速闪切(非阻塞,节奏由同长度 wait 控制):真心话与谎话交替独占屏幕,
|
||||
/// 两者的屏幕底色(电视头背景)不同,越到后面切得越快,结束定格在真话帧。
|
||||
/// <<expression_truth_lie_flicker "没问题|冇问题|帽问题" "我就是个笑话" 3.2 0.24 12>>
|
||||
/// <<expression_truth_lie_flicker "l10n.hs.exp.actor.loop" "l10n.hs.exp.log1.target" 3.2 0.24 12>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_truth_lie_flicker")]
|
||||
public static void ExpressionTruthLieFlicker(
|
||||
public static IEnumerator ExpressionTruthLieFlicker(
|
||||
string liePhrases,
|
||||
string truthPhrase,
|
||||
float duration,
|
||||
float interval = 0.24f,
|
||||
float fontSize = 12f)
|
||||
{
|
||||
if (TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
presentation.StartTruthLieFlicker(liePhrases, truthPhrase, duration, interval, fontSize);
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
liePhrases,
|
||||
truthPhrase);
|
||||
if (localized == null || localized.Length != 2 ||
|
||||
!ValidateRequiredText(localized[0], "expression_truth_lie_flicker", nameof(liePhrases)) ||
|
||||
!ValidateRequiredText(localized[1], "expression_truth_lie_flicker", nameof(truthPhrase)))
|
||||
yield break;
|
||||
|
||||
presentation.StartTruthLieFlicker(localized[0], localized[1], duration, interval, fontSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定格的谎话被 glitch 翻转成真话(阻塞):大字乱跳中长度从谎话渐变到真话、真假底色抢闪,
|
||||
/// 最后落在暖色真话。紧接 expression_memory_cut + expression_truth_snap 收尾。
|
||||
/// <<expression_lie_morph_truth "没问题" "我就是个笑话" 0.9>>
|
||||
/// <<expression_lie_morph_truth "l10n.hs.exp.atk.why" "l10n.hs.exp.log1.target" 0.9>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_lie_morph_truth")]
|
||||
public static IEnumerator ExpressionLieMorphTruth(
|
||||
@@ -412,8 +580,19 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
lieText,
|
||||
truthText);
|
||||
if (localized == null || localized.Length != 2 ||
|
||||
!ValidateRequiredText(localized[0], "expression_lie_morph_truth", nameof(lieText)) ||
|
||||
!ValidateRequiredText(localized[1], "expression_lie_morph_truth", nameof(truthText)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.MorphLieToTruth(lieText, truthText, duration));
|
||||
presentation.MorphLieToTruth(localized[0], localized[1], duration));
|
||||
}
|
||||
|
||||
[YarnCommand("expression_truth_leak")]
|
||||
@@ -421,24 +600,44 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
fragment);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_truth_leak", nameof(fragment)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.LeakTruthFragment(fragment, duration));
|
||||
presentation.LeakTruthFragment(localized[0], duration));
|
||||
}
|
||||
|
||||
[YarnCommand("expression_lie_shuffle")]
|
||||
public static void ExpressionLieShuffle(
|
||||
public static IEnumerator ExpressionLieShuffle(
|
||||
string characterPool,
|
||||
float duration,
|
||||
float interval)
|
||||
{
|
||||
if (TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
presentation.StartLieCharacterShuffle(characterPool, duration, interval);
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
characterPool);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_lie_shuffle", nameof(characterPool)))
|
||||
yield break;
|
||||
|
||||
presentation.StartLieCharacterShuffle(localized[0], duration, interval);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 演员大字闪现(阻塞):抖动的粒子字瞬间隐藏,屏幕中央出现白色大字(红青色散双影+急速缩放),
|
||||
/// 停顿 holdDuration 秒后始终恢复粒子字抖动;结尾定格请使用 expression_actor_flash_in。
|
||||
/// <<expression_actor_flash "没问题" 0.6 10.5 1.15 1.60 0.03 0.22 0.5 0.2>>
|
||||
/// <<expression_actor_flash "l10n.hs.exp.atk.why" 0.6 10.5 1.15 1.60 0.03 0.22 0.5 0.2>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_actor_flash")]
|
||||
public static IEnumerator ExpressionActorFlash(
|
||||
@@ -454,9 +653,19 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
text);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_actor_flash", nameof(text)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.FlashActorLie(
|
||||
text,
|
||||
localized[0],
|
||||
holdDuration,
|
||||
fontSize,
|
||||
settledScale,
|
||||
@@ -470,7 +679,7 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <summary>
|
||||
/// 演员大字入场并保持(阻塞):入场表现与 expression_actor_flash 相同,但不执行退场,
|
||||
/// 适合在段落结尾定格。后续清场命令仍会正常移除它。
|
||||
/// <<expression_actor_flash_in "没问题" 0.35 10.5 1.1 1.45 0.008 0.06 0.5 0>>
|
||||
/// <<expression_actor_flash_in "l10n.hs.exp.atk.why" 0.35 10.5 1.1 1.45 0.008 0.06 0.5 0>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_actor_flash_in")]
|
||||
public static IEnumerator ExpressionActorFlashIn(
|
||||
@@ -486,9 +695,19 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
text);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_actor_flash_in", nameof(text)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.FlashActorLie(
|
||||
text,
|
||||
localized[0],
|
||||
holdDuration,
|
||||
fontSize,
|
||||
settledScale,
|
||||
@@ -505,10 +724,36 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <<expression_actor_flash_loop_start>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_actor_flash_loop_start")]
|
||||
public static void ExpressionActorFlashLoopStart()
|
||||
public static IEnumerator ExpressionActorFlashLoopStart()
|
||||
{
|
||||
if (TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
presentation.StartActorFlashLoop();
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
ExpressionContentCatalog catalog = ExpressionManager.ContentCatalog;
|
||||
if (catalog == null || !LocalizationKit.IsLocalizedParam(catalog.ActorLoopReference))
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
"[LanguageYarnCommand] Actor Loop 本地化引用未配置;命令已安全结束。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
catalog.ActorLoopReference);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_actor_flash_loop_start", "ActorLoopReference"))
|
||||
yield break;
|
||||
|
||||
if (!TryParseExpressionTokens(localized[0], out List<string> phrases))
|
||||
{
|
||||
UnityEngine.Debug.LogError(
|
||||
"[LanguageYarnCommand] Actor Loop 短句列表无效;命令已安全结束。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
presentation.StartActorFlashLoop(phrases);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -525,7 +770,7 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <summary>
|
||||
/// 真话闪现(阻塞):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍;
|
||||
/// "被掐断"由紧随其后的 expression_glitch_pulse 表现。不推进谎话冲击等级。
|
||||
/// <<expression_actor_truth_flash "我就是个笑话" 0.35 8.5 0.55 0.02>>
|
||||
/// <<expression_actor_truth_flash "l10n.hs.exp.log1.target" 0.35 8.5 0.55 0.02>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_actor_truth_flash")]
|
||||
public static IEnumerator ExpressionActorTruthFlash(
|
||||
@@ -537,25 +782,52 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
text);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_actor_truth_flash", nameof(text)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.FlashActorTruth(text, holdDuration, fontSize, alpha, jitter));
|
||||
presentation.FlashActorTruth(localized[0], holdDuration, fontSize, alpha, jitter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。
|
||||
/// truthPool 非空时其字符也混入换字池,并以真话暖色显示。
|
||||
/// <<expression_actor_slot "没问题冇" 5 0.05 14 "我就是个笑话">>
|
||||
/// <<expression_actor_slot "l10n.hs.exp.actor.loop" 5 0.05 14 "l10n.hs.exp.log1.target">>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_actor_slot")]
|
||||
public static void ExpressionActorSlot(
|
||||
public static IEnumerator ExpressionActorSlot(
|
||||
string charPool,
|
||||
float duration,
|
||||
float interval = 0.05f,
|
||||
float fontSize = 14f,
|
||||
string truthPool = "")
|
||||
{
|
||||
if (TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
presentation.StartActorSlotMachine(charPool, duration, interval, fontSize, truthPool);
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
charPool,
|
||||
truthPool);
|
||||
if (localized == null || localized.Length != 2 ||
|
||||
!ValidateRequiredText(localized[0], "expression_actor_slot", nameof(charPool)))
|
||||
yield break;
|
||||
|
||||
presentation.StartActorSlotMachine(
|
||||
localized[0],
|
||||
duration,
|
||||
interval,
|
||||
fontSize,
|
||||
localized[1] ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -583,7 +855,7 @@ namespace AibisDream.MiniGame.Language
|
||||
|
||||
/// <summary>
|
||||
/// 单字/短词满屏重击,并等待该拍结束。
|
||||
/// <<expression_actor_word_hit "没" 0.16 18 1.55>>
|
||||
/// <<expression_actor_word_hit "l10n.hs.exp.atk.why" 0.16 18 1.55>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_actor_word_hit")]
|
||||
public static IEnumerator ExpressionActorWordHit(
|
||||
@@ -594,13 +866,23 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
text);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_actor_word_hit", nameof(text)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.PlayActorWordHit(text, holdDuration, fontSize, punchScale));
|
||||
presentation.PlayActorWordHit(localized[0], holdDuration, fontSize, punchScale));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单列词组严格裁在表情屏内,像字幕一样无缝向下滚动。
|
||||
/// <<expression_actor_scroll "没问题|冇问题|帽问题" 1.8 0.85 0.45 5.2>>
|
||||
/// <<expression_actor_scroll "l10n.hs.exp.actor.loop" 1.8 0.85 0.45 5.2>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_actor_scroll")]
|
||||
public static IEnumerator ExpressionActorScroll(
|
||||
@@ -612,9 +894,19 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
phraseList);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_actor_scroll", nameof(phraseList)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.ScrollActorLies(
|
||||
phraseList,
|
||||
localized[0],
|
||||
scrollDuration,
|
||||
loopDuration,
|
||||
holdDuration,
|
||||
@@ -742,7 +1034,7 @@ namespace AibisDream.MiniGame.Language
|
||||
/// <summary>
|
||||
/// 用现有目标粒子完整显示一个红色攻击词,并等待确定性的冲击动作结束。
|
||||
/// 超过目标粒子数量时会安全跳过,不会截断。
|
||||
/// <<expression_truth_attack "为什么" 0.34 0.45>>
|
||||
/// <<expression_truth_attack "l10n.hs.exp.atk.why" 0.34 0.45>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_truth_attack")]
|
||||
public static IEnumerator ExpressionTruthAttack(
|
||||
@@ -753,17 +1045,26 @@ namespace AibisDream.MiniGame.Language
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
text);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_truth_attack", nameof(text)))
|
||||
yield break;
|
||||
|
||||
// 红屏武装时每个攻击词都把屏幕顶一下(punch),力度跟着这个词的 impact 走。
|
||||
// 写在这里而不是 Yarn 里逐条加 pulse:攻击列表经常重调,两边手写必然脱节。
|
||||
ExpressionManager.PunchScreenForAttack(impact);
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.PlayTruthAttack(text, duration, impact));
|
||||
presentation.PlayTruthAttack(localized[0], duration, impact));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keep an already resolved truth phrase in place while it gradually becomes unstable and red.
|
||||
/// <<expression_truth_destabilize "不要那样看我" 1.6 0.13 22 "FF304D">>
|
||||
/// <<expression_truth_destabilize "l10n.hs.exp.log3.target" 1.6 0.13 22 "FF304D">>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_truth_destabilize")]
|
||||
public static IEnumerator ExpressionTruthDestabilize(
|
||||
@@ -775,9 +1076,19 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
text);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_truth_destabilize", nameof(text)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.DestabilizeTruthPhrase(
|
||||
text,
|
||||
localized[0],
|
||||
duration,
|
||||
shakeAmplitude,
|
||||
shakeSpeed,
|
||||
@@ -786,7 +1097,7 @@ namespace AibisDream.MiniGame.Language
|
||||
|
||||
/// <summary>
|
||||
/// 将当前攻击词重组为固定青绿色真话,并等待稳定完成。
|
||||
/// <<expression_truth_settle "我想赢一次" 0.8>>
|
||||
/// <<expression_truth_settle "l10n.hs.exp.final.win" 0.8>>
|
||||
/// </summary>
|
||||
[YarnCommand("expression_truth_settle")]
|
||||
public static IEnumerator ExpressionTruthSettle(
|
||||
@@ -795,8 +1106,18 @@ namespace AibisDream.MiniGame.Language
|
||||
{
|
||||
if (!TryGetPresentation(out LogReleasePresentationController presentation))
|
||||
yield break;
|
||||
|
||||
string[] localized = null;
|
||||
yield return ResolveTextReferences(
|
||||
GetCommandLocale(),
|
||||
values => localized = values,
|
||||
text);
|
||||
if (localized == null || localized.Length != 1 ||
|
||||
!ValidateRequiredText(localized[0], "expression_truth_settle", nameof(text)))
|
||||
yield break;
|
||||
|
||||
yield return ExpressionManager.StartCoroutine(
|
||||
presentation.SettleTruthPhrase(text, duration));
|
||||
presentation.SettleTruthPhrase(localized[0], duration));
|
||||
}
|
||||
|
||||
[YarnCommand("expression_truth_blackout")]
|
||||
|
||||
@@ -808,14 +808,19 @@ namespace AibisDream.MiniGame.Language
|
||||
}
|
||||
}
|
||||
|
||||
public void StartActorFlashLoop()
|
||||
public void StartActorFlashLoop(IReadOnlyList<string> phrases)
|
||||
{
|
||||
EnsureRuntimeObjects();
|
||||
if (!ExpectState(nameof(StartActorFlashLoop), PresentationState.Memory))
|
||||
return;
|
||||
if (phrases == null || phrases.Count == 0)
|
||||
{
|
||||
Debug.LogError("[LogReleasePresentation] Actor Flash Loop 没有有效短句;已安全跳过。");
|
||||
return;
|
||||
}
|
||||
|
||||
StopActorFlashLoopImmediate(true);
|
||||
actorFlashLoopRoutine = StartCoroutine(ActorFlashLoopRoutine());
|
||||
actorFlashLoopRoutine = StartCoroutine(ActorFlashLoopRoutine(phrases));
|
||||
}
|
||||
|
||||
public void StopActorFlashLoop()
|
||||
@@ -823,27 +828,33 @@ namespace AibisDream.MiniGame.Language
|
||||
StopActorFlashLoopImmediate(true);
|
||||
}
|
||||
|
||||
private IEnumerator ActorFlashLoopRoutine()
|
||||
private IEnumerator ActorFlashLoopRoutine(IReadOnlyList<string> phrases)
|
||||
{
|
||||
while (state == PresentationState.Memory)
|
||||
{
|
||||
yield return FlashActorLie(
|
||||
"没问题", 0.25f, 10.5f, 1.1f, 1.45f, 0.008f, 0.06f, 0.50f, 0f);
|
||||
if (state != PresentationState.Memory)
|
||||
break;
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
for (int i = 0; i < phrases.Count && state == PresentationState.Memory; i++)
|
||||
{
|
||||
int style = i % 3;
|
||||
float hold = style == 2 ? 0.15f : 0.25f;
|
||||
float fontSize = style == 0 ? 10.5f : style == 1 ? 11.2f : 12f;
|
||||
float punchScale = style == 0 ? 1.45f : style == 1 ? 1.85f : 2.20f;
|
||||
float chromaDistance = style == 2 ? 0.010f : 0.008f;
|
||||
float chromaAlpha = style == 2 ? 0.08f : 0.06f;
|
||||
float impact = style == 0 ? 0.50f : style == 1 ? 0.72f : 1f;
|
||||
|
||||
yield return FlashActorLie(
|
||||
"冇问题", 0.25f, 11.2f, 1.1f, 1.85f, 0.008f, 0.06f, 0.72f, 0f);
|
||||
if (state != PresentationState.Memory)
|
||||
break;
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
|
||||
yield return FlashActorLie(
|
||||
"帽问题", 0.15f, 12.0f, 1.1f, 2.20f, 0.010f, 0.08f, 1.00f, 0f);
|
||||
if (state != PresentationState.Memory)
|
||||
break;
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
yield return FlashActorLie(
|
||||
phrases[i],
|
||||
hold,
|
||||
fontSize,
|
||||
1.1f,
|
||||
punchScale,
|
||||
chromaDistance,
|
||||
chromaAlpha,
|
||||
impact,
|
||||
0f);
|
||||
if (state == PresentationState.Memory)
|
||||
yield return new WaitForSeconds(0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
actorFlashLoopRoutine = null;
|
||||
|
||||
Reference in New Issue
Block a user