feat: 硬编码本地化

This commit is contained in:
2026-07-28 21:34:42 +08:00
parent ca5b714124
commit a48e4babd4
37 changed files with 558 additions and 732 deletions
+1
View File
@@ -53,6 +53,7 @@ namespace AibisDream.Utility
public const string UITextTable = "UIText";
public const string ChapterInfoTable = "ChapterInfo";
public const string ParamsTable = "Params";
public const string ExpressParticlePoolParam = "l10n.hs.exp.pool";
#endregion
@@ -210,6 +210,22 @@ namespace AibisDream.Framework
return IsLocalizedParam(key) ? key[LocalizationPrefix.Length..] : key;
}
/// <summary>
/// 判断解析结果是否是当前 <c>l10n.</c> 引用对应的缺失标记。
/// 非本地化原始文本即使使用相同括号格式也不会被误判。
/// </summary>
public static bool IsMissingParamResult(string source, string localizedValue)
{
if (!IsLocalizedParam(source))
return false;
string key = GetL10NParamKey(source);
string marker = string.IsNullOrEmpty(key)
? "⟦empty⟧"
: $"⟦{key}⟧";
return string.Equals(localizedValue, marker, StringComparison.Ordinal);
}
/// <summary>
/// 确保本地化系统已初始化
/// </summary>
@@ -230,7 +230,7 @@ namespace AibisDream.MiniGame.Language
}
/// <summary>
/// 目标粒子:chineseChars 随机单字;非目标粒子:从笑话/干扰短句字符中随机。
/// 目标粒子:从本轮本地化默认池随机;非目标粒子:从笑话/干扰短句字符中随机。
/// 使用 IsTarget 判断,与红蓝颜色解绑。
/// </summary>
protected override string GetNextDisplayChar()
@@ -264,7 +264,7 @@ namespace AibisDream.MiniGame.Language
private void UpdateFocusInterferenceCharacters()
{
if (focusInterferenceProgress <= 0f ||
string.IsNullOrEmpty(overrideCharPool) ||
!HasOverrideCharacterPool ||
(!isStatic && !isCalmed))
{
return;
@@ -54,12 +54,10 @@ namespace AibisDream.MiniGame.Language
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)
@@ -77,9 +75,6 @@ namespace AibisDream.MiniGame.Language
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)
{
@@ -20,15 +20,6 @@ namespace AibisDream
[SerializeField] private ExpressionScreenPresentationController screenPresentation;
[SerializeField] private ExpressionContentCatalog contentCatalog;
[Header("默认配置")]
[SerializeField] private List<string> defaultAnxietyPhrases = new List<string>
{
"哈哈",
"嘿嘿",
"呵呵"
};
[SerializeField] private string defaultTargetSentence = "这是一个测试示例";
[Header("Sprite 淡入淡出")]
[SerializeField] private SpriteRenderer screenOverlayRenderer;
[SerializeField] private SpriteRenderer volcanoOverlayRenderer;
@@ -162,20 +153,37 @@ namespace AibisDream
/// </summary>
/// <param name="anxietyPhrases">干扰短句列表(笑话等,非目标粒子从中随机取字符)</param>
/// <param name="targetSentence">目标句子</param>
/// <param name="defaultCharacterPool">当前 Locale 的默认随机字符池</param>
/// <param name="completionNodeName">完成时触发的对话节点名称(可选)</param>
/// <param name="nonTargetParticleTotal">非目标候选粒子总数;&lt;0 时使用 Inspector 默认候选池大小</param>
public void StartSystem(List<string> anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1)
public void StartSystem(
List<string> anxietyPhrases,
string targetSentence,
IReadOnlyList<string> defaultCharacterPool,
string completionNodeName = null,
int nonTargetParticleTotal = -1)
{
HideSpriteOverlaysForMinigame();
if (particleManager != null)
{
particleManager.SetPresentationController(logReleasePresentation);
// 使用传入的配置,如果没有则使用默认配置
var phrases = anxietyPhrases ?? defaultAnxietyPhrases;
var sentence = targetSentence ?? defaultTargetSentence;
particleManager.InitializeSystem(phrases, sentence, completionNodeName, nonTargetParticleTotal);
if (anxietyPhrases == null || anxietyPhrases.Count == 0 ||
string.IsNullOrWhiteSpace(targetSentence) ||
defaultCharacterPool == null ||
defaultCharacterPool.Count == 0)
{
Debug.LogError(
"[ExpressionManager] 启动参数缺少目标文字、干扰 token 或默认字符池;系统未启动。");
return;
}
particleManager.InitializeSystem(
anxietyPhrases,
targetSentence,
defaultCharacterPool,
completionNodeName,
nonTargetParticleTotal);
}
}
@@ -211,10 +219,20 @@ namespace AibisDream
/// <summary>
/// 启动粒子系统(字符串数组版本,方便 Yarn 调用)
/// </summary>
public void StartSystem(string[] anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1)
public void StartSystem(
string[] anxietyPhrases,
string targetSentence,
IReadOnlyList<string> defaultCharacterPool,
string completionNodeName = null,
int nonTargetParticleTotal = -1)
{
List<string> phrasesList = anxietyPhrases != null ? new List<string>(anxietyPhrases) : null;
StartSystem(phrasesList, targetSentence, completionNodeName, nonTargetParticleTotal);
StartSystem(
phrasesList,
targetSentence,
defaultCharacterPool,
completionNodeName,
nonTargetParticleTotal);
}
/// <summary>
@@ -921,28 +939,6 @@ namespace AibisDream
targetOpacity);
}
public IEnumerator BeginExpressionLie(string keyword, string preset)
{
if (logReleasePresentation == null)
{
Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;谎话命令安全结束。");
yield break;
}
yield return logReleasePresentation.BeginLie(keyword, preset);
}
public IEnumerator BreakExpressionLie()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.BreakLie();
}
public IEnumerator RevealExpressionTruth()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.RevealTruth();
}
public IEnumerator EndExpressionMemory()
{
if (logReleasePresentation == null) yield break;
@@ -4,8 +4,8 @@ using System.Globalization;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 将玩家可见文字按 Unicode 文本元素拆分。空白保留在布局中,但不生成粒子。
/// 随机粒子字符池不使用本类,维持现有行为
/// 将玩家可见文字按 Unicode 文本元素拆分。
/// 空白保留在布局中但不生成粒子;随机字符池直接忽略空白
/// </summary>
public static class ExpressionTextTokenizer
{
@@ -39,6 +39,17 @@ namespace AibisDream.MiniGame.Language
return result;
}
public static List<string> BuildCharacterPool(IEnumerable<string> texts)
{
var result = new List<string>();
if (texts == null)
return result;
foreach (string text in texts)
result.AddRange(GetVisibleElements(text));
return result;
}
public static Layout BuildLayout(
string text,
float characterSpacing,
@@ -64,8 +64,10 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private float textMargin = 1f;
[Header("游戏配置")]
[SerializeField] private List<string> anxietyPhrases = new List<string>(); // 干扰短句(笑话等,用于非目标粒子,Yarn 会覆盖)
[SerializeField] private string targetSentence = "这是一个测试示例"; // 目标句子
private List<string> anxietyPhrases = new List<string>();
private string targetSentence = string.Empty;
private IReadOnlyList<string> defaultCharacterPool = new List<string>().AsReadOnly();
private IReadOnlyList<string> interferenceCharacterPool = new List<string>().AsReadOnly();
[SerializeField] private string completionDialogNode = ""; // 完成时触发的对话节点
[Header("波形参数")]
@@ -346,15 +348,39 @@ namespace AibisDream.MiniGame.Language
/// </summary>
/// <param name="phrases">焦虑短句列表</param>
/// <param name="sentence">目标句子</param>
/// <param name="localizedDefaultPool">当前 Locale 的默认随机字符池</param>
/// <param name="dialogNode">完成时触发的对话节点</param>
/// <param name="nonTargetParticleTotal">
/// 非目标候选粒子总数(蓝/干扰侧可交互粒子数量)。≥0 时候选池大小 = 目标句字数 + 该值;&lt;0 时使用 Inspector 的 Candidate Count。
/// </param>
public void InitializeSystem(List<string> phrases, string sentence, string dialogNode = null, int nonTargetParticleTotal = -1)
public void InitializeSystem(
List<string> phrases,
string sentence,
IReadOnlyList<string> localizedDefaultPool,
string dialogNode = null,
int nonTargetParticleTotal = -1)
{
CaptureDefaultCandidateCountFromInspector();
string useSentence = string.IsNullOrEmpty(sentence) ? "这是一个测试示例" : sentence;
if (phrases == null || phrases.Count == 0 || string.IsNullOrWhiteSpace(sentence))
{
Debug.LogError(
"[LanguageParticleManager] 目标文字或干扰 token 为空;初始化已中止。");
return;
}
List<string> resolvedDefaultPool =
ExpressionTextTokenizer.BuildCharacterPool(localizedDefaultPool);
List<string> resolvedInterferencePool =
ExpressionTextTokenizer.BuildCharacterPool(phrases);
if (resolvedDefaultPool.Count == 0 || resolvedInterferencePool.Count == 0)
{
Debug.LogError(
"[LanguageParticleManager] 默认字符池或干扰字符池没有有效的 Unicode 文本元素;初始化已中止。");
return;
}
string useSentence = sentence;
int visibleTargetCount = ExpressionTextTokenizer.GetVisibleElements(useSentence).Count;
if (visibleTargetCount == 0)
{
@@ -373,6 +399,13 @@ namespace AibisDream.MiniGame.Language
candidateCount = defaultCandidateCountFromInspector;
}
// 在创建或复用粒子前先提交本轮不可变快照,防止首帧或上一轮字符泄漏。
anxietyPhrases = new List<string>(phrases);
targetSentence = useSentence;
completionDialogNode = dialogNode ?? "";
defaultCharacterPool = resolvedDefaultPool.AsReadOnly();
interferenceCharacterPool = resolvedInterferencePool.AsReadOnly();
// 如果是第一次初始化,先执行基础初始化
if (!isInitialized)
{
@@ -407,16 +440,9 @@ namespace AibisDream.MiniGame.Language
else
{
EnsureCandidatePoolSize(candidateCount);
ApplyCharacterPoolsToParticles();
}
// 设置游戏配置
anxietyPhrases = phrases ?? new List<string>();
targetSentence = useSentence;
completionDialogNode = dialogNode ?? "";
// 设置全局焦虑短句配置
TextParticle.SetAnxietyPhrases(anxietyPhrases);
// 根据目标句子更新目标粒子数量
redParticleCount = Mathf.Min(visibleTargetCount, candidateCount);
@@ -700,6 +726,7 @@ namespace AibisDream.MiniGame.Language
Vector3 pos = GetRandomPositionInBounds();
GameObject obj = CreateParticleObject(pos, $"FloatingParticle_{i}");
FloatingTextParticle particle = obj.AddComponent<FloatingTextParticle>();
particle.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool);
particle.SetFont(chineseFontAsset, chineseFontMaterial);
particle.SetColor(floatingTextColor);
particle.SetFontSize(floatingTextFontSize);
@@ -744,6 +771,7 @@ namespace AibisDream.MiniGame.Language
Vector3 pos = GetRandomPositionInBounds();
GameObject obj = CreateParticleObject(pos, $"CandidateParticle_{index}");
CandidateParticle particle = obj.AddComponent<CandidateParticle>();
particle.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool);
particle.SetFont(chineseFontAsset, chineseFontMaterial);
particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor);
particle.SetNonTargetMarquee(
@@ -760,6 +788,22 @@ namespace AibisDream.MiniGame.Language
candidateParticles.Add(particle);
}
private void ApplyCharacterPoolsToParticles()
{
foreach (CandidateParticle particle in candidateParticles)
particle?.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool);
foreach (FloatingTextParticle particle in floatingParticles)
particle?.SetCharacterPools(defaultCharacterPool, interferenceCharacterPool);
}
private void RefreshAllRandomCharacters()
{
foreach (CandidateParticle particle in candidateParticles)
particle?.InitializeRandomCharacter();
foreach (FloatingTextParticle particle in floatingParticles)
particle?.InitializeRandomCharacter();
}
/// <summary>
/// 增删候选粒子以匹配目标数量(用于 Yarn 动态指定非目标粒子总数后复用同一视图)
/// </summary>
@@ -2016,6 +2060,8 @@ namespace AibisDream.MiniGame.Language
Debug.LogWarning("[LanguageParticleManager] 老虎机换字字符池为空;已安全跳过。");
return;
}
IReadOnlyList<string> poolElements =
ExpressionTextTokenizer.GetVisibleElements(pool).AsReadOnly();
float changeInterval = Mathf.Max(0.02f, interval);
foreach (TargetFocusVisual visual in focusVisuals.Values)
@@ -2023,7 +2069,7 @@ namespace AibisDream.MiniGame.Language
if (visual?.particle == null)
continue;
visual.particle.SetOverrideCharPool(pool);
visual.particle.SetOverrideCharacterPool(poolElements);
visual.particle.SetChangeInterval(changeInterval);
visual.particle.ResetChangeTimer(changeInterval);
visual.particle.isStatic = false;
@@ -2040,8 +2086,12 @@ namespace AibisDream.MiniGame.Language
{
if (string.IsNullOrEmpty(charPool))
return null;
string cleaned = charPool.Replace(" ", string.Empty).Replace("|", string.Empty);
return cleaned.Length == 0 ? null : cleaned;
string withoutSeparators = charPool
.Replace("|", string.Empty)
.Replace("", string.Empty);
List<string> elements =
ExpressionTextTokenizer.GetVisibleElements(withoutSeparators);
return elements.Count == 0 ? null : string.Concat(elements);
}
public void StopSlotMachineShuffle()
@@ -2053,7 +2103,7 @@ namespace AibisDream.MiniGame.Language
}
foreach (CandidateParticle particle in targetParticles)
particle?.SetOverrideCharPool(null);
particle?.SetOverrideCharacterPool(null);
}
private IEnumerator EndSlotMachineShuffleAfter(float duration)
@@ -2061,7 +2111,7 @@ namespace AibisDream.MiniGame.Language
yield return new WaitForSeconds(duration);
slotMachineRoutine = null;
foreach (CandidateParticle particle in targetParticles)
particle?.SetOverrideCharPool(null);
particle?.SetOverrideCharacterPool(null);
}
public void PrepareTruthReleaseFromLie()
@@ -3151,6 +3201,7 @@ namespace AibisDream.MiniGame.Language
RebuildScreenClipMaterials();
SelectRedParticles();
RefreshAllRandomCharacters();
UpdateStatusUI();
if (playEntranceEffect)
{
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using AibisDream.FixSystem;
using AibisDream;
using AibisDream.Framework;
using AibisDream.Utility;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using Yarn.Unity;
@@ -38,9 +39,19 @@ namespace AibisDream.MiniGame.Language
while (!all.IsCompleted)
yield return null;
completed?.Invoke(all.Status == TaskStatus.RanToCompletion
? all.Result
: System.Array.Empty<string>());
if (all.Status != TaskStatus.RanToCompletion)
{
completed?.Invoke(System.Array.Empty<string>());
yield break;
}
string[] values = all.Result;
for (int i = 0; i < values.Length; i++)
{
if (LocalizationKit.IsMissingParamResult(references[i], values[i]))
values[i] = null;
}
completed?.Invoke(values);
}
private static bool ValidateRequiredText(string value, string command, string parameter)
@@ -77,6 +88,22 @@ namespace AibisDream.MiniGame.Language
return tokens.Count > 0;
}
public static bool TryParseExpressionPool(
string value,
out List<string> characters)
{
characters = new List<string>();
if (string.IsNullOrWhiteSpace(value) ||
value.Contains('|') ||
value.Contains(''))
{
return false;
}
characters = ExpressionTextTokenizer.GetVisibleElements(value);
return characters.Count > 0;
}
private static bool TryGetPresentation(out LogReleasePresentationController presentation)
{
presentation = ExpressionManager != null
@@ -130,10 +157,12 @@ namespace AibisDream.MiniGame.Language
locale,
values => localized = values,
anxietyPhrasesStr,
targetSentence);
if (localized == null || localized.Length != 2 ||
targetSentence,
ConstRef.ExpressParticlePoolParam);
if (localized == null || localized.Length != 3 ||
!ValidateRequiredText(localized[0], "start_expression", nameof(anxietyPhrasesStr)) ||
!ValidateRequiredText(localized[1], "start_expression", nameof(targetSentence)))
!ValidateRequiredText(localized[1], "start_expression", nameof(targetSentence)) ||
!ValidateRequiredText(localized[2], "start_expression", "defaultCharacterPool"))
yield break;
if (!TryParseExpressionTokens(localized[0], out List<string> phrases))
@@ -142,6 +171,12 @@ namespace AibisDream.MiniGame.Language
"[LanguageYarnCommand] start_expression 没有有效的干扰 token;命令已安全结束。");
yield break;
}
if (!TryParseExpressionPool(localized[2], out List<string> defaultCharacterPool))
{
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] start_expression 的默认字符池无效;命令已安全结束。");
yield break;
}
// 先清理上一轮演出,再淡出旧文字/连线并恢复 Log 操作界面。
yield return ExpressionManager.PreparePresentationForNextRound();
@@ -151,6 +186,7 @@ namespace AibisDream.MiniGame.Language
ExpressionManager.StartSystem(
phrases,
localized[1],
defaultCharacterPool,
completionNode,
nonTargetParticleTotal);
yield return ExpressionManager.WaitUntilExpressionFlowReady();
@@ -199,10 +235,12 @@ namespace AibisDream.MiniGame.Language
locale,
values => localized = values,
round.TokenReference,
round.TargetReference);
if (localized == null || localized.Length != 2 ||
round.TargetReference,
ConstRef.ExpressParticlePoolParam);
if (localized == null || localized.Length != 3 ||
!ValidateRequiredText(localized[0], "start_expression_round", nameof(round.TokenReference)) ||
!ValidateRequiredText(localized[1], "start_expression_round", nameof(round.TargetReference)))
!ValidateRequiredText(localized[1], "start_expression_round", nameof(round.TargetReference)) ||
!ValidateRequiredText(localized[2], "start_expression_round", "defaultCharacterPool"))
yield break;
if (!TryParseExpressionTokens(localized[0], out List<string> phrases))
@@ -211,6 +249,12 @@ namespace AibisDream.MiniGame.Language
$"[LanguageYarnCommand] 表达轮次 '{roundId}' 没有有效的干扰 token。");
yield break;
}
if (!TryParseExpressionPool(localized[2], out List<string> defaultCharacterPool))
{
UnityEngine.Debug.LogError(
$"[LanguageYarnCommand] 表达轮次 '{roundId}' 的默认字符池无效。");
yield break;
}
yield return ExpressionManager.PreparePresentationForNextRound();
yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f);
@@ -219,6 +263,7 @@ namespace AibisDream.MiniGame.Language
ExpressionManager.StartSystem(
phrases,
localized[1],
defaultCharacterPool,
completionNode ?? string.Empty,
round.NonTargetParticleCount);
yield return ExpressionManager.WaitUntilExpressionFlowReady();
@@ -379,49 +424,6 @@ namespace AibisDream.MiniGame.Language
presentation.BeginFaceEntry(duration));
}
[YarnCommand("expression_lie_begin")]
public static IEnumerator ExpressionLieBegin(string keyword, string preset)
{
if (ExpressionManager == null)
{
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(localized[0], preset));
}
[YarnCommand("expression_lie_break")]
public static IEnumerator ExpressionLieBreak()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.BreakExpressionLie());
}
[YarnCommand("expression_truth_reveal")]
public static IEnumerator ExpressionTruthReveal()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.RevealExpressionTruth());
}
// 以下命令是一动作一命令。非 IEnumerator 命令只启动效果,节拍由 Yarn 的 wait 控制。
[YarnCommand("expression_lie_prepare")]
@@ -540,7 +542,7 @@ namespace AibisDream.MiniGame.Language
/// <summary>
/// 结尾真假快速闪切(非阻塞,节奏由同长度 wait 控制):真心话与谎话交替独占屏幕,
/// 两者的屏幕底色(电视头背景)不同,越到后面切得越快,结束定格在真话帧。
/// <<expression_truth_lie_flicker "l10n.hs.exp.actor.loop" "l10n.hs.exp.log1.target" 3.2 0.24 12>>
/// <<expression_truth_lie_flicker "l10n.hs.exp.log1.tokens" "l10n.hs.exp.log1.target" 3.2 0.24 12>>
/// </summary>
[YarnCommand("expression_truth_lie_flicker")]
public static IEnumerator ExpressionTruthLieFlicker(
@@ -719,54 +721,6 @@ namespace AibisDream.MiniGame.Language
true));
}
/// <summary>
/// 非阻塞启动“没问题 → 冇问题 → 帽问题”三拍无限闪现循环。
/// <<expression_actor_flash_loop_start>>
/// </summary>
[YarnCommand("expression_actor_flash_loop_start")]
public static IEnumerator ExpressionActorFlashLoopStart()
{
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>
/// 立即停止无限闪现循环、清除当前大字,并恢复晃动粒子字。
/// <<expression_actor_flash_loop_stop>>
/// </summary>
[YarnCommand("expression_actor_flash_loop_stop")]
public static void ExpressionActorFlashLoopStop()
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.StopActorFlashLoop();
}
/// <summary>
/// 真话闪现(阻塞):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍;
/// "被掐断"由紧随其后的 expression_glitch_pulse 表现。不推进谎话冲击等级。
@@ -799,7 +753,7 @@ namespace AibisDream.MiniGame.Language
/// <summary>
/// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。
/// truthPool 非空时其字符也混入换字池,并以真话暖色显示。
/// <<expression_actor_slot "l10n.hs.exp.actor.loop" 5 0.05 14 "l10n.hs.exp.log1.target">>
/// <<expression_actor_slot "l10n.hs.exp.log1.tokens" 5 0.05 14 "l10n.hs.exp.log1.target">>
/// </summary>
[YarnCommand("expression_actor_slot")]
public static IEnumerator ExpressionActorSlot(
@@ -882,7 +836,7 @@ namespace AibisDream.MiniGame.Language
/// <summary>
/// 单列词组严格裁在表情屏内,像字幕一样无缝向下滚动。
/// <<expression_actor_scroll "l10n.hs.exp.actor.loop" 1.8 0.85 0.45 5.2>>
/// <<expression_actor_scroll "l10n.hs.exp.log1.tokens" 1.8 0.85 0.45 5.2>>
/// </summary>
[YarnCommand("expression_actor_scroll")]
public static IEnumerator ExpressionActorScroll(
@@ -38,34 +38,19 @@ namespace AibisDream.MiniGame.Language
{
public readonly string Name;
public readonly float BaseDistortion;
public readonly float LiePause;
public readonly float BreakPeak;
public readonly float BreakDuration;
public readonly float TruthDuration;
public readonly float TruthPeripheral;
public Preset(
string name,
float baseDistortion,
float liePause,
float breakPeak,
float breakDuration,
float truthDuration,
float truthPeripheral)
float baseDistortion)
{
Name = name;
BaseDistortion = baseDistortion;
LiePause = liePause;
BreakPeak = breakPeak;
BreakDuration = breakDuration;
TruthDuration = truthDuration;
TruthPeripheral = truthPeripheral;
}
}
private static readonly Preset LightPreset = new Preset("Light", 0.10f, 0.35f, 0.25f, 0.25f, 0.70f, 0.15f);
private static readonly Preset MediumPreset = new Preset("Medium", 0.20f, 0.55f, 0.50f, 0.35f, 0.90f, 0.30f);
private static readonly Preset HeavyPreset = new Preset("Heavy", 0.35f, 0.80f, 0.85f, 0.50f, 1.10f, 0.50f);
private static readonly Preset LightPreset = new Preset("Light", 0.10f);
private static readonly Preset MediumPreset = new Preset("Medium", 0.20f);
private static readonly Preset HeavyPreset = new Preset("Heavy", 0.35f);
[Header("Scene References")]
[SerializeField] private LanguageParticleManager particleManager;
@@ -96,7 +81,6 @@ namespace AibisDream.MiniGame.Language
[Header("Screen Styling")]
[SerializeField] private Color lieColor = new Color(0.66f, 0.94f, 1f, 1f);
[SerializeField] private Color errorColor = new Color(1f, 0.34f, 0.18f, 1f);
[SerializeField] private Color truthWarmColor = new Color(1f, 1f, 1f, 1f);
[Tooltip("爆发段真话闪现与老虎机真话字符的暖色;需与谎话的冷白明显区分。")]
[SerializeField] private Color truthAccentColor = new Color(1f, 0.84f, 0.6f, 1f);
@@ -118,7 +102,6 @@ namespace AibisDream.MiniGame.Language
private Preset currentPreset = LightPreset;
private MemoryKind currentMemoryKind = MemoryKind.Office;
private string currentLieKeyword = string.Empty;
private string currentLeakFragment = string.Empty;
private Transform runtimeRoot;
private SpriteRenderer memoryRendererA;
@@ -129,7 +112,6 @@ namespace AibisDream.MiniGame.Language
private SpriteMask screenSpriteMask;
private TextMeshPro lieText;
private TextMeshPro lieGhostText;
private TextMeshPro systemText;
private TextMeshPro actorFlashText;
private TextMeshPro actorFlashChromaRed;
private TextMeshPro actorFlashChromaCyan;
@@ -141,7 +123,6 @@ namespace AibisDream.MiniGame.Language
private string truthWarmColorHex;
private TMPRectClipper lieClipper;
private TMPRectClipper lieGhostClipper;
private TMPRectClipper systemClipper;
private TMPRectClipper actorFlashClipper;
private TMPRectClipper actorFlashChromaRedClipper;
private TMPRectClipper actorFlashChromaCyanClipper;
@@ -150,7 +131,6 @@ namespace AibisDream.MiniGame.Language
private bool actorFlashActive;
private int actorFlashCount;
private Coroutine actorOverdriveRoutine;
private Coroutine actorFlashLoopRoutine;
private Texture2D runtimeWhiteTexture;
private Sprite runtimeWhiteSprite;
private MaterialPropertyBlock memoryBlockA;
@@ -254,7 +234,6 @@ namespace AibisDream.MiniGame.Language
TweenMemoryOpacity(memoryRendererB, 0f, duration);
FadeText(lieText, 0f, duration);
FadeText(lieGhostText, 0f, duration);
FadeText(systemText, 0f, duration);
FadeText(actorFlashText, 0f, duration);
FadeText(actorFlashChromaRed, 0f, duration);
FadeText(actorFlashChromaCyan, 0f, duration);
@@ -337,7 +316,6 @@ namespace AibisDream.MiniGame.Language
currentPreset = preset;
currentMemoryKind = kind;
currentLeakFragment = GetLeakFragment(kind);
_ = horizontalSpeed;
actorFlashCount = 0;
memoryImpact = 0f;
@@ -398,84 +376,6 @@ namespace AibisDream.MiniGame.Language
Mathf.Max(0.01f, duration));
}
public IEnumerator BeginLie(string keyword, string presetName)
{
EnsureRuntimeObjects();
if (!TryResolvePreset(presetName, out Preset requestedPreset))
{
Debug.LogError($"[LogReleasePresentation] 未知演出预设“{presetName}”;命令安全结束。");
yield break;
}
if (!string.Equals(requestedPreset.Name, currentPreset.Name, StringComparison.OrdinalIgnoreCase))
{
Debug.LogWarning(
$"[LogReleasePresentation] expression_lie_begin 的预设 {requestedPreset.Name} " +
$"与当前记忆 {currentPreset.Name} 不一致,将沿用当前记忆预设。");
}
PrepareLieVisuals(0.25f, 0.005f);
SqueezeTruthCharacters(
0.22f,
0.055f + currentPreset.BreakPeak * 0.065f,
0.28f);
ShowLieKeyword(keyword, 0.22f, 14f);
PlayScreenScanline(0.32f, currentMemoryKind == MemoryKind.Sunset);
if (currentMemoryKind == MemoryKind.PrivateOffice)
ShowSystemMessage("输出正常", "normal", 0.12f);
else
HideSystemMessageImmediate();
yield return new WaitForSeconds(0.25f);
}
public IEnumerator BreakLie()
{
EnsureRuntimeObjects();
if (!ExpectState(nameof(BreakLie), PresentationState.LieHolding))
yield break;
yield return new WaitForSeconds(currentPreset.LiePause);
BeginLieCracking(0.48f);
PlayScreenScanline(0.22f, true);
PlayScreenDim(0.28f, 0.06f, 0.12f);
PlayMemoryJolt(0.08f, 0.24f, 0.11f);
yield return LeakTruthFragment(currentLeakFragment, 0.24f);
if (currentMemoryKind == MemoryKind.PrivateOffice)
{
HideSystemMessage(0.01f);
yield return new WaitForSeconds(0.08f);
ShowSystemMessage("输出与原始 log 不一致", "error", 0.08f);
yield return PulseGlitch(HeavyPreset.BreakPeak, 0.12f, 0.18f);
}
}
public IEnumerator RevealTruth()
{
EnsureRuntimeObjects();
if (!ExpectState(
nameof(RevealTruth),
PresentationState.LieCracking,
PresentationState.LieHolding))
{
yield break;
}
DOTween.Kill(this);
yield return HoldTruthBlackout(0.055f, 0.42f);
SplitLieVisual(currentPreset.BreakDuration, 0.18f, 1.85f, 0.42f);
PlayTruthFlash(0.62f, 0.045f, 0.025f, 0.16f);
PlayMemoryBrightnessPulse(0.16f, 0.06f, 0.20f);
PlayMemoryTear(currentPreset.BreakPeak, currentPreset.BreakDuration);
PlayGlitchPulse(currentPreset.BreakPeak, 0.12f, currentPreset.BreakDuration);
PlayFaceDip(currentPreset.BreakDuration, faceDipRatio);
ReleaseTruthCharacters(0.08f);
yield return ResolveTruthCharacters(
currentPreset.TruthDuration,
currentPreset.TruthPeripheral,
0.42f);
}
public void PrepareLieVisuals(float freezeDuration, float noiseTarget)
{
EnsureRuntimeObjects();
@@ -560,42 +460,6 @@ namespace AibisDream.MiniGame.Language
PlayScanline(Mathf.Max(0.01f, duration), stalled);
}
public void ShowSystemMessage(string value, string style, float fadeDuration)
{
EnsureRuntimeObjects();
if (!ExpectState(
nameof(ShowSystemMessage),
PresentationState.FaceEntering,
PresentationState.Memory,
PresentationState.LieHolding,
PresentationState.LieCracking))
return;
Color color;
if (string.Equals(style, "error", StringComparison.OrdinalIgnoreCase))
{
color = errorColor;
}
else
{
if (!string.Equals(style, "normal", StringComparison.OrdinalIgnoreCase))
Debug.LogWarning($"[LogReleasePresentation] 未知系统提示样式“{style}”,改用 normal。");
color = new Color(0.58f, 0.78f, 0.82f, 1f);
}
SetSystemMessage(value ?? string.Empty, color, Mathf.Max(0.01f, fadeDuration));
}
public void HideSystemMessage(float fadeDuration)
{
if (systemText == null || !systemText.gameObject.activeSelf)
return;
systemText.DOKill();
systemText.DOFade(0f, Mathf.Max(0.01f, fadeDuration))
.SetEase(Ease.OutQuad)
.SetTarget(this)
.OnComplete(HideSystemMessageImmediate);
}
public void BeginLieCracking(float lieAlpha)
{
if (!ExpectState(
@@ -654,9 +518,9 @@ namespace AibisDream.MiniGame.Language
PresentationState.LieCracking))
yield break;
currentLeakFragment = fragment ?? string.Empty;
string resolvedFragment = fragment ?? string.Empty;
yield return ShuffleLieCharactersRoutine(
currentLeakFragment,
resolvedFragment,
Mathf.Max(0.01f, duration),
0.045f);
}
@@ -688,7 +552,6 @@ namespace AibisDream.MiniGame.Language
return;
DOTween.Kill(this);
StopActorFlashLoopImmediate(false);
StopActorOverdriveImmediate();
memoryPushSpeed = 0f;
memoryPushProgress = 0f;
@@ -808,73 +671,6 @@ namespace AibisDream.MiniGame.Language
}
}
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(phrases));
}
public void StopActorFlashLoop()
{
StopActorFlashLoopImmediate(true);
}
private IEnumerator ActorFlashLoopRoutine(IReadOnlyList<string> phrases)
{
while (state == PresentationState.Memory)
{
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(
phrases[i],
hold,
fontSize,
1.1f,
punchScale,
chromaDistance,
chromaAlpha,
impact,
0f);
if (state == PresentationState.Memory)
yield return new WaitForSeconds(0.3f);
}
}
actorFlashLoopRoutine = null;
}
private void StopActorFlashLoopImmediate(bool restoreParticles)
{
if (actorFlashLoopRoutine != null)
{
StopCoroutine(actorFlashLoopRoutine);
actorFlashLoopRoutine = null;
}
actorFlashActive = false;
memoryImpact = 0f;
HideActorFlashVisualsImmediate();
if (restoreParticles && state == PresentationState.Memory)
particleManager?.SetTargetParticlesAlpha(1f, 0f);
}
/// <summary>
/// 真话闪现:谎话大字之间,暖色完整真话带着细微抖动挣扎浮现一拍。
/// 不推进谎话冲击等级、不累积记忆损伤;"被掐断"由紧随其后的 expression_glitch_pulse 表现。
@@ -1100,7 +896,6 @@ namespace AibisDream.MiniGame.Language
state = PresentationState.TruthResolving;
StopLieShuffleImmediate(true);
HideLieLayersImmediate();
HideSystemMessageImmediate();
StopScanline();
StopScreenDim();
@@ -1706,7 +1501,6 @@ namespace AibisDream.MiniGame.Language
return;
state = PresentationState.TruthResolving;
HideSystemMessage(0.15f);
particleManager?.PrepareTruthReleaseFromLie();
particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor);
particleManager?.SetTargetParticlesAlpha(1f, Mathf.Max(0.01f, alphaDuration));
@@ -1752,7 +1546,6 @@ namespace AibisDream.MiniGame.Language
}
HideLieLayersImmediate();
HideSystemMessageImmediate();
StopScanline();
StopScreenDim();
ReleaseScreenTextClipMaterials();
@@ -1790,7 +1583,6 @@ namespace AibisDream.MiniGame.Language
TweenMemoryOpacity(memoryRendererA, 0f, memoryExitDuration);
TweenMemoryOpacity(memoryRendererB, 0f, memoryExitDuration);
HideLieLayersImmediate();
HideSystemMessageImmediate();
StopScanline();
StopScreenDim();
SilenceNoiseOverlayIfPresent();
@@ -1815,7 +1607,6 @@ namespace AibisDream.MiniGame.Language
{
DOTween.Kill(this);
StopAllCoroutines();
actorFlashLoopRoutine = null;
RestoreFaceSpriteImmediate();
memoryOpacityA = 0f;
@@ -1824,7 +1615,6 @@ namespace AibisDream.MiniGame.Language
DisableMemoryRenderer(memoryRendererB);
activeMemoryRenderer = null;
HideLieLayersImmediate();
HideSystemMessageImmediate();
StopScanline();
StopScreenDim();
ReleaseScreenTextClipMaterials();
@@ -1847,7 +1637,6 @@ namespace AibisDream.MiniGame.Language
particleManager?.RestoreTargetPresentationDefaults();
state = PresentationState.Idle;
currentLieKeyword = string.Empty;
currentLeakFragment = string.Empty;
memoryPushSpeed = 0f;
memoryPushProgress = 0f;
memoryZoom = 1f;
@@ -2309,21 +2098,18 @@ namespace AibisDream.MiniGame.Language
CreateScreenMask();
lieText = CreateScreenText("LieKeyword", 4.1f, screenTextSortingOrder);
lieGhostText = CreateScreenText("LieKeywordGhost", 4.1f, screenTextSortingOrder - 1);
systemText = CreateScreenText("SystemPrompt", 1.35f, screenTextSortingOrder + 1);
actorFlashText = CreateScreenText("ActorFlashKeyword", 10.5f, screenTextSortingOrder);
actorFlashChromaRed = CreateScreenText("ActorFlashChromaRed", 10.5f, screenTextSortingOrder - 1);
actorFlashChromaCyan = CreateScreenText("ActorFlashChromaCyan", 10.5f, screenTextSortingOrder - 1);
truthFlashText = CreateScreenText("ActorTruthFlash", 8.5f, screenTextSortingOrder - 1);
lieClipper = lieText.gameObject.AddComponent<TMPRectClipper>();
lieGhostClipper = lieGhostText.gameObject.AddComponent<TMPRectClipper>();
systemClipper = systemText.gameObject.AddComponent<TMPRectClipper>();
actorFlashClipper = actorFlashText.gameObject.AddComponent<TMPRectClipper>();
actorFlashChromaRedClipper = actorFlashChromaRed.gameObject.AddComponent<TMPRectClipper>();
actorFlashChromaCyanClipper = actorFlashChromaCyan.gameObject.AddComponent<TMPRectClipper>();
truthFlashClipper = truthFlashText.gameObject.AddComponent<TMPRectClipper>();
lieClipper.Initialize(lieText, expressionScreenMaskRect);
lieGhostClipper.Initialize(lieGhostText, expressionScreenMaskRect);
systemClipper.Initialize(systemText, expressionScreenMaskRect);
actorFlashClipper.Initialize(actorFlashText, expressionScreenMaskRect);
actorFlashChromaRedClipper.Initialize(actorFlashChromaRed, expressionScreenMaskRect);
actorFlashChromaCyanClipper.Initialize(actorFlashChromaCyan, expressionScreenMaskRect);
@@ -2437,15 +2223,12 @@ namespace AibisDream.MiniGame.Language
PositionText(actorFlashChromaRed, center, screen.size);
PositionText(actorFlashChromaCyan, center, screen.size);
PositionText(truthFlashText, center, screen.size);
Vector3 systemPosition = center + Vector3.up * screen.extents.y * 0.70f;
PositionText(systemText, systemPosition, new Vector2(screen.size.x, screen.size.y * 0.22f));
}
private void EnsureScreenTextClipMaterials()
{
lieClipper?.Initialize(lieText, expressionScreenMaskRect);
lieGhostClipper?.Initialize(lieGhostText, expressionScreenMaskRect);
systemClipper?.Initialize(systemText, expressionScreenMaskRect);
actorFlashClipper?.Initialize(actorFlashText, expressionScreenMaskRect);
actorFlashChromaRedClipper?.Initialize(actorFlashChromaRed, expressionScreenMaskRect);
actorFlashChromaCyanClipper?.Initialize(actorFlashChromaCyan, expressionScreenMaskRect);
@@ -2458,7 +2241,6 @@ namespace AibisDream.MiniGame.Language
{
lieClipper?.ReleaseMaterial();
lieGhostClipper?.ReleaseMaterial();
systemClipper?.ReleaseMaterial();
actorFlashClipper?.ReleaseMaterial();
actorFlashChromaRedClipper?.ReleaseMaterial();
actorFlashChromaCyanClipper?.ReleaseMaterial();
@@ -2751,22 +2533,6 @@ namespace AibisDream.MiniGame.Language
screenDimRenderer.gameObject.SetActive(false);
}
private void SetSystemMessage(string value, Color color, float fadeDuration)
{
SetTextActive(systemText, value, color);
SetTextAlpha(systemText, 0f);
FadeText(systemText, 1f, fadeDuration);
}
private void HideSystemMessageImmediate()
{
if (systemText == null)
return;
systemText.DOKill();
systemText.text = string.Empty;
systemText.gameObject.SetActive(false);
}
private void HideLieLayersImmediate()
{
StopLieShuffleImmediate(false);
@@ -2777,7 +2543,6 @@ namespace AibisDream.MiniGame.Language
private void HideActorFlashImmediate()
{
StopActorFlashLoopImmediate(false);
StopActorOverdriveImmediate();
StopTruthLieFlicker();
actorFlashActive = false;
@@ -2885,25 +2650,6 @@ namespace AibisDream.MiniGame.Language
}
}
private static bool TryResolvePreset(string value, out Preset preset)
{
switch ((value ?? string.Empty).Trim().ToLowerInvariant())
{
case "light":
preset = LightPreset;
return true;
case "medium":
preset = MediumPreset;
return true;
case "heavy":
preset = HeavyPreset;
return true;
default:
preset = LightPreset;
return false;
}
}
private static Preset GetPresetForMemory(MemoryKind kind)
{
return kind switch
@@ -2914,16 +2660,6 @@ namespace AibisDream.MiniGame.Language
};
}
private static string GetLeakFragment(MemoryKind kind)
{
return kind switch
{
MemoryKind.Office => "笑话",
MemoryKind.Sunset => "离开",
_ => "不要那样看我"
};
}
private bool ExpectState(string command, params PresentationState[] allowed)
{
for (int i = 0; i < allowed.Length; i++)
@@ -26,7 +26,7 @@ Floating Count: 100
Candidate Count: 50
Red Particle Count: 8
Connection Distance: 2
Target Sentence: "别过来我感觉害怕"
Target Sentence: 由启动命令传入,不在 Inspector 中保存测试文案
Waveform Amplitude: 0.3
Interaction Radius: 0.5
```
@@ -68,7 +68,7 @@ Interaction Radius: 0.5
1. Window → TextMeshPro → Font Asset Creator
2. 选择支持中文的字体(如:思源黑体、微软雅黑)
3. Character Set: Custom Characters
4. 粘贴`TextParticle.cs`中的`chineseChars`字符
4. 粘贴 `Params/hs.exp.pool` 及正式 Express 文案所需字符
5. Generate Font Atlas
### 看不到粒子?
@@ -50,7 +50,7 @@ Language/
#### 游戏参数
- **Connection Distance**: 连接距离阈值(默认2,Unity单位)
- **Target Sentence**: 目标句子(默认"别过来我感觉害怕"
- **Target Sentence**: 目标句子(必须由启动命令传入
#### 波形参数
- **Waveform Amplitude**: 波形振幅(默认0.3
@@ -91,7 +91,7 @@ Language/
完成第一阶段后:
1. 红色粒子自动排列成水平一行
2. 蓝色粒子淡出
3. 红色粒子逐个揭示目标文字"别过来我感觉害怕"
3. 红色粒子逐个揭示启动命令传入的目标文字
4. 文字出现剧烈抖动(焦虑效果)
### 第三阶段:波形交互
@@ -158,15 +158,12 @@ Language/
### 添加新的文字内容
修改`CandidateParticle.cs`中的字符集:
```csharp
protected static string chineseChars = "你的字符集...";
protected static string[] chineseWords = { "词组1", "词组2", ... };
```
在 Unity Localization 的 `Params/hs.exp.pool` 中配置当前语言的随机字符池。
字符池按 Unicode 文本元素拆分,忽略空白;重复字符可用于提高出现权重。
### 修改目标句子
在Manager的Inspector中修改`Target Sentence`字段
修改 `ExpressionContentCatalog` 对应轮次引用的 `Params` 本地化条目。
### 调整波形特性
@@ -27,7 +27,11 @@ namespace AibisDream.MiniGame.Language
protected float changeTimer;
protected float changeInterval = 1f;
protected string overrideCharPool;
private static readonly IReadOnlyList<string> EmptyCharacterPool =
new List<string>().AsReadOnly();
protected IReadOnlyList<string> defaultCharacterPool = EmptyCharacterPool;
protected IReadOnlyList<string> interferenceCharacterPool = EmptyCharacterPool;
protected IReadOnlyList<string> overrideCharacterPool = EmptyCharacterPool;
protected Bounds movementBounds;
protected bool useCircularBounds = false;
protected Vector3 boundsCenter;
@@ -36,13 +40,6 @@ namespace AibisDream.MiniGame.Language
private Material customFontMaterial;
private TMPRectClipper screenClipper;
// 字符集
protected static string chineseChars = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞";
protected static string[] chineseWords = { "不安", "紧张", "焦虑", "担忧", "烦躁", "恐慌", "恐惧", "绝望", "痛苦", "悲伤", "愤怒", "孤独", "无助", "迷茫", "困惑", "压抑", "沉重", "疲惫", "空虚", "失落" };
// 动态配置的干扰短句(笑话/焦虑等,由 Yarn start_expression 传入,用于非目标粒子字符池)
protected static List<string> configuredAnxietyPhrases = new List<string>();
protected virtual void Awake()
{
if (textMesh == null)
@@ -85,7 +82,7 @@ namespace AibisDream.MiniGame.Language
SyncLayerToChildren();
changeTimer = Random.Range(0.5f, 1.5f);
currentChar = GetNextDisplayChar();
currentChar = string.Empty;
UpdateText();
// TMP 重建 mesh / SubMesh 后再对齐一次 layer
SyncLayerToChildren();
@@ -112,7 +109,7 @@ namespace AibisDream.MiniGame.Language
// 边界检测
CheckBounds();
// 字符变化:目标粒子用 chineseChars 随机,非目标粒子从干扰短句字符中随机
// 字符变化:目标粒子用本轮默认池,非目标粒子从干扰短句字符中随机
changeTimer -= Time.deltaTime;
if (changeTimer <= 0)
{
@@ -209,7 +206,7 @@ namespace AibisDream.MiniGame.Language
protected string GetRandomChar()
{
return chineseChars[Random.Range(0, chineseChars.Length)].ToString();
return GetRandomPoolElement(defaultCharacterPool);
}
/// <summary>
@@ -218,22 +215,14 @@ namespace AibisDream.MiniGame.Language
/// </summary>
protected string GetRandomCharFromPhrases()
{
if (configuredAnxietyPhrases == null || configuredAnxietyPhrases.Count == 0)
if (interferenceCharacterPool == null || interferenceCharacterPool.Count == 0)
return GetRandomChar();
var chars = new List<char>();
foreach (var phrase in configuredAnxietyPhrases)
{
if (string.IsNullOrEmpty(phrase)) continue;
foreach (char c in phrase)
chars.Add(c);
}
if (chars.Count == 0) return GetRandomChar();
return chars[Random.Range(0, chars.Count)].ToString();
return GetRandomPoolElement(interferenceCharacterPool);
}
/// <summary>
/// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。
/// 默认返回 chineseChars 随机单字
/// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素
/// </summary>
protected virtual string GetNextDisplayChar()
{
@@ -247,27 +236,60 @@ namespace AibisDream.MiniGame.Language
/// </summary>
public void SetOverrideCharPool(string pool)
{
overrideCharPool = string.IsNullOrEmpty(pool) ? null : pool;
SetOverrideCharacterPool(
ExpressionTextTokenizer.GetVisibleElements(pool));
}
public void SetOverrideCharacterPool(IReadOnlyList<string> pool)
{
overrideCharacterPool = pool != null && pool.Count > 0
? pool
: EmptyCharacterPool;
}
protected bool TryGetOverridePoolChar(out string character)
{
if (string.IsNullOrEmpty(overrideCharPool))
if (!HasOverrideCharacterPool)
{
character = null;
return false;
}
character = overrideCharPool[Random.Range(0, overrideCharPool.Length)].ToString();
character = GetRandomPoolElement(overrideCharacterPool);
return true;
}
/// <summary>
/// 设置全局干扰短句配置(笑话等,由 LanguageParticleManager 调用)
/// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。
/// </summary>
public static void SetAnxietyPhrases(List<string> phrases)
public void SetCharacterPools(
IReadOnlyList<string> defaultPool,
IReadOnlyList<string> interferencePool)
{
configuredAnxietyPhrases = phrases ?? new List<string>();
defaultCharacterPool = defaultPool != null && defaultPool.Count > 0
? defaultPool
: EmptyCharacterPool;
interferenceCharacterPool =
interferencePool != null && interferencePool.Count > 0
? interferencePool
: EmptyCharacterPool;
}
public void InitializeRandomCharacter()
{
currentChar = GetNextDisplayChar();
UpdateText();
changeTimer = Random.Range(0.5f, 1.5f);
}
protected bool HasOverrideCharacterPool =>
overrideCharacterPool != null && overrideCharacterPool.Count > 0;
private static string GetRandomPoolElement(IReadOnlyList<string> pool)
{
if (pool == null || pool.Count == 0)
return string.Empty;
return pool[Random.Range(0, pool.Count)];
}
protected void UpdateText()