Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
T
2026-07-28 21:34:42 +08:00

1200 lines
50 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
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;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 表达系统的 Yarn 命令接口(原语言系统)
/// </summary>
public static class LanguageYarnCommand
{
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;
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)
{
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;
}
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
? ExpressionManager.LogReleasePresentation
: null;
if (presentation != null)
return true;
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] LogReleasePresentationController 未找到;演出命令安全结束。");
return false;
}
/// <summary>
/// 启动表达粒子系统(在打开视图后调用),等粒子字入场完成即可返回(可交互);panel 动效并行不阻塞。
/// 流程:若有上一轮则先淡出字与连线 → 再淡入火山释放 log → 再启动新一轮并等粒子就绪。
/// 注意:Yarn Spinner 2.x 对同名 <see cref="YarnCommand"/> 重载支持不可靠,只保留一个注册入口,用可选参数区分 3/4 参调用。
/// <<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>
/// <param name="targetSentence">目标句子</param>
/// <param name="completionNode">完成时触发的对话节点(可空字符串)</param>
/// <param name="nonTargetParticleTotal">非目标候选粒子总数;&lt;0 表示使用 Inspector 默认</param>
[YarnCommand("start_expression")]
public static IEnumerator StartExpression(
string anxietyPhrasesStr,
string targetSentence,
string completionNode = "",
float nonTargetParticleTotal = -1f)
{
return RunStartExpression(
anxietyPhrasesStr,
targetSentence,
completionNode ?? "",
UnityEngine.Mathf.RoundToInt(nonTargetParticleTotal));
}
private static IEnumerator RunStartExpression(string anxietyPhrasesStr, string targetSentence, string completionNode, int nonTargetParticleTotal)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!请确保场景中有 ExpressionManager 组件。");
yield break;
}
Locale locale = LocalizationSettings.SelectedLocale;
string[] localized = null;
yield return ResolveTextReferences(
locale,
values => localized = values,
anxietyPhrasesStr,
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[2], "start_expression", "defaultCharacterPool"))
yield break;
if (!TryParseExpressionTokens(localized[0], out List<string> phrases))
{
UnityEngine.Debug.LogError(
"[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();
yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f);
yield return ExpressionManager.EnsureReleaseLogFadedIn(1f);
ExpressionManager.SetActiveExpressionLocale(locale);
ExpressionManager.StartSystem(
phrases,
localized[1],
defaultCharacterPool,
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,
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[2], "start_expression_round", "defaultCharacterPool"))
yield break;
if (!TryParseExpressionTokens(localized[0], out List<string> phrases))
{
UnityEngine.Debug.LogError(
$"[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);
yield return ExpressionManager.EnsureReleaseLogFadedIn(1f);
ExpressionManager.SetActiveExpressionLocale(locale);
ExpressionManager.StartSystem(
phrases,
localized[1],
defaultCharacterPool,
completionNode ?? string.Empty,
round.NonTargetParticleCount);
yield return ExpressionManager.WaitUntilExpressionFlowReady();
}
/// <summary>
/// 关闭表达系统
/// <<close_expression>>
/// </summary>
[YarnCommand("close_expression")]
public static void CloseExpression()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
return;
}
ExpressionManager.CloseView();
}
/// <summary>
/// 重置表达系统(使用当前配置)
/// <<reset_expression>>
/// 或
/// <<reset_expression false>> // 不播放入场动画
/// </summary>
/// <param name="playEntranceEffect">是否播放入场效果</param>
[YarnCommand("reset_expression")]
public static void ResetExpression(bool playEntranceEffect = true)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
return;
}
ExpressionManager.ResetSystem(playEntranceEffect);
}
/// <summary>
/// 触发聚焦稳定化(从文字飘动保持态 → 概率递增 → 文字锁定 → 排列),等待全流程完成后返回
/// <<resolve_expression_focus>>
/// </summary>
[YarnCommand("resolve_expression_focus")]
public static IEnumerator ResolveExpressionFocus()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.ResolveFocusAndWait());
}
/// <summary>
/// 淡入 ExpressionManager 的 Sprite 图片
/// <<expression_sprite_fade_in>>
/// <<expression_sprite_fade_in 1.5>>
/// </summary>
/// <param name="duration">淡入时长(秒),默认 1</param>
[YarnCommand("expression_sprite_fade_in")]
public static IEnumerator ExpressionSpriteFadeIn(float duration = 1f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.FadeInSpriteOverlay(duration));
}
/// <summary>
/// 淡出 ExpressionManager 的 Sprite 图片
/// <<expression_sprite_fade_out>>
/// <<expression_sprite_fade_out 1.5>>
/// </summary>
/// <param name="duration">淡出时长(秒),默认 1</param>
[YarnCommand("expression_sprite_fade_out")]
public static IEnumerator ExpressionSpriteFadeOut(float duration = 1f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.FadeOutSpriteOverlay(duration));
}
/// <summary>
/// Glitch 噪波过渡,等待完成后再继续下一步。
/// <<glitch_transition 0.95 2>> -- 从 0 到 0.952 秒
/// <<glitch_transition 0.95>> -- 立即到 0.95duration 默认 0
/// <<glitch_transition 0.3 0.45 1>> -- 从 0.3 到 0.451 秒
/// </summary>
[YarnCommand("glitch_transition")]
public static IEnumerator GlitchTransition(float to, float duration = 0f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.TransitionGlitchAndWait(to, duration));
}
/// <summary>
/// 记忆淡入(非阻塞):透明度在 fadeDuration 内显现,画面同时按 pushSpeed 持续推近;
/// targetOpacity 是本次淡入的目标透明度。horizontalSpeed 为旧 Yarn 兼容参数,基础层不再横移。
/// 进记忆前可用 glitch_transition 铺 noisebegin 时会把 noise 淡出,改由记忆材质
/// _SimGlitch / Analog 承接轻微失真。preserveGlitch 已忽略。争夺段 jolt/tear/impact 仍叠加。
/// </summary>
[YarnCommand("expression_memory_begin")]
public static void ExpressionMemoryBegin(
string memoryKey,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
bool preserveGlitch = false,
float targetOpacity = 1f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
return;
}
ExpressionManager.StartCoroutine(
ExpressionManager.BeginExpressionMemory(
memoryKey,
fadeDuration,
pushSpeed,
horizontalSpeed,
preserveGlitch,
targetOpacity));
}
/// <summary>
/// 随对白把当前记忆透明度推进到目标值(非阻塞)。
/// <<expression_memory_alpha 0.65 0.45>>
/// </summary>
[YarnCommand("expression_memory_alpha")]
public static void ExpressionMemoryAlpha(float target, float duration = 0.4f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SetActiveMemoryOpacity(target, duration);
}
/// <summary>
/// 显式淡入火山脸,并等待淡入完成。
/// <<expression_face_fade_in 0.45>>
/// </summary>
[YarnCommand("expression_face_fade_in")]
public static IEnumerator ExpressionFaceFadeIn(float duration = 0.45f)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.BeginFaceEntry(duration));
}
// 以下命令是一动作一命令。非 IEnumerator 命令只启动效果,节拍由 Yarn 的 wait 控制。
[YarnCommand("expression_lie_prepare")]
public static void ExpressionLiePrepare(float freezeDuration, float noiseTarget)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PrepareLieVisuals(freezeDuration, noiseTarget);
}
[YarnCommand("expression_truth_squeeze")]
public static void ExpressionTruthSqueeze(float duration, float jitter, float alpha)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SqueezeTruthCharacters(duration, jitter, alpha);
}
[YarnCommand("expression_lie_crack_begin")]
public static void ExpressionLieCrackBegin(float lieAlpha)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.BeginLieCracking(lieAlpha);
}
[YarnCommand("expression_screen_dim")]
public static void ExpressionScreenDim(float alpha, float riseDuration, float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayScreenDim(alpha, riseDuration, fallDuration);
}
[YarnCommand("expression_memory_jolt")]
public static void ExpressionMemoryJolt(float offset, float duration, float tear)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayMemoryJolt(offset, duration, tear);
}
/// <summary>
/// 调节记忆的轻微失真扰动(非阻塞)。默认强度由演出预设决定,本命令用于演出中微调。
/// <<expression_memory_sim 0.55 0.3>>
/// </summary>
[YarnCommand("expression_memory_sim")]
public static void ExpressionMemorySim(float target, float duration = 0.3f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SetSimulationGlitch(target, duration);
}
/// <summary>
/// 真话聚合尝试(非阻塞):目标字被慢慢拉向最终位置并按字序逐个锁定成真心话。
/// target 小于 1 时最后几个字永远差一点锁不住——"感觉快要成功了"。
/// <<expression_truth_gather 0.9 3.2>>
/// </summary>
[YarnCommand("expression_truth_gather")]
public static void ExpressionTruthGather(float target = 0.9f, float duration = 3f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.BeginTruthGatherAttempt(target, duration);
}
/// <summary>
/// 真话聚合被打断(非阻塞):目标字向外爆散并恢复乱跳。burst 为世界单位的爆散距离。
/// <<expression_truth_scatter 0.9>>
/// </summary>
[YarnCommand("expression_truth_scatter")]
public static void ExpressionTruthScatter(float burst = 0.9f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.ScatterTruthGatherAttempt(burst);
}
/// <summary>
/// 真话聚合挣扎(非阻塞):向 target 推进的途中反复"够到一点又被拽回去",
/// 已锁的字会崩开重新乱跳。挣扎感来自聚合曲线本身,不使用任何颜色或额外特效。
/// <<expression_truth_strain 0.86 3.2 3>>
/// </summary>
/// <param name="target">最终停留的聚合进度</param>
/// <param name="duration">整段时长</param>
/// <param name="tugs">回抽次数(0~4);&lt;=0 时退化为 expression_truth_gather</param>
[YarnCommand("expression_truth_strain")]
public static void ExpressionTruthStrain(float target = 0.85f, float duration = 3f, float tugs = 2f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
{
presentation.BeginTruthStrainAttempt(
target,
duration,
UnityEngine.Mathf.RoundToInt(tugs));
}
}
/// <summary>
/// 真话聚合崩塌(非阻塞):进度被一口气拽回 0,字沿原路退回悬浮位并恢复乱跳。
/// 与 expression_truth_scatter 的区别:scatter 是径向爆散,collapse 是"被拽回去"。
/// <<expression_truth_collapse 0.45>>
/// </summary>
[YarnCommand("expression_truth_collapse")]
public static void ExpressionTruthCollapse(float duration = 0.45f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.CollapseTruthGatherAttempt(duration);
}
/// <summary>
/// 火山脸错误闪红(非阻塞):聚合失败时短暂替换红屏 Sprite,随后恢复,并轻微下沉。
/// 注意:LOG1/LOG2 的失败信号已明确不使用红色与脸部报错,此命令仅保留兼容。
/// <<expression_face_error 0.5 0.85>>
/// </summary>
[YarnCommand("expression_face_error")]
public static void ExpressionFaceError(float duration = 0.5f, float intensity = 0.85f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayFaceErrorFlash(duration, intensity);
}
/// <summary>
/// 结尾真假快速闪切(非阻塞,节奏由同长度 wait 控制):真心话与谎话交替独占屏幕,
/// 两者的屏幕底色(电视头背景)不同,越到后面切得越快,结束定格在真话帧。
/// <<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(
string liePhrases,
string truthPhrase,
float duration,
float interval = 0.24f,
float fontSize = 12f)
{
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 "l10n.hs.exp.atk.why" "l10n.hs.exp.log1.target" 0.9>>
/// </summary>
[YarnCommand("expression_lie_morph_truth")]
public static IEnumerator ExpressionLieMorphTruth(
string lieText,
string truthText,
float duration = 0.9f)
{
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(localized[0], localized[1], duration));
}
[YarnCommand("expression_truth_leak")]
public static IEnumerator ExpressionTruthLeak(string fragment, float duration)
{
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(localized[0], duration));
}
[YarnCommand("expression_lie_shuffle")]
public static IEnumerator ExpressionLieShuffle(
string characterPool,
float duration,
float 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 "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(
string text,
float holdDuration = 0.6f,
float fontSize = 10.5f,
float settledScale = -1f,
float punchScale = -1f,
float chromaDistance = -1f,
float chromaAlpha = -1f,
float impact = -1f,
float glitchPeak = -1f)
{
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(
localized[0],
holdDuration,
fontSize,
settledScale,
punchScale,
chromaDistance,
chromaAlpha,
impact,
glitchPeak));
}
/// <summary>
/// 演员大字入场并保持(阻塞):入场表现与 expression_actor_flash 相同,但不执行退场,
/// 适合在段落结尾定格。后续清场命令仍会正常移除它。
/// <<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(
string text,
float holdDuration = 0.35f,
float fontSize = 10.5f,
float settledScale = -1f,
float punchScale = -1f,
float chromaDistance = -1f,
float chromaAlpha = -1f,
float impact = -1f,
float glitchPeak = -1f)
{
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(
localized[0],
holdDuration,
fontSize,
settledScale,
punchScale,
chromaDistance,
chromaAlpha,
impact,
glitchPeak,
true));
}
/// <summary>
/// 真话闪现(阻塞):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍;
/// "被掐断"由紧随其后的 expression_glitch_pulse 表现。不推进谎话冲击等级。
/// <<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(
string text,
float holdDuration = 0.35f,
float fontSize = 8.5f,
float alpha = 0.8f,
float jitter = 0.02f)
{
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(localized[0], holdDuration, fontSize, alpha, jitter));
}
/// <summary>
/// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。
/// truthPool 非空时其字符也混入换字池,并以真话暖色显示。
/// <<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(
string charPool,
float duration,
float interval = 0.05f,
float fontSize = 14f,
string 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>
/// 将 Glitch、记忆冲击与画面推近缓慢堆到峰值,停住后保持峰值,等待后续硬切。
/// <<expression_overload_peak 0.75 0.22 1.10 1 1>>
/// </summary>
[YarnCommand("expression_overload_peak")]
public static IEnumerator ExpressionOverloadPeak(
float riseDuration = 0.75f,
float holdDuration = 0.22f,
float zoom = 1.10f,
float glitchPeak = 1f,
float impact = 1f)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.BuildActorOverloadPeak(
riseDuration,
holdDuration,
zoom,
glitchPeak,
impact));
}
/// <summary>
/// 单字/短词满屏重击,并等待该拍结束。
/// <<expression_actor_word_hit "l10n.hs.exp.atk.why" 0.16 18 1.55>>
/// </summary>
[YarnCommand("expression_actor_word_hit")]
public static IEnumerator ExpressionActorWordHit(
string text,
float holdDuration = 0.16f,
float fontSize = 18f,
float punchScale = 1.55f)
{
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(localized[0], holdDuration, fontSize, punchScale));
}
/// <summary>
/// 单列词组严格裁在表情屏内,像字幕一样无缝向下滚动。
/// <<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(
string phraseList,
float scrollDuration = 1.8f,
float loopDuration = 0.85f,
float holdDuration = 0.45f,
float fontSize = 5.2f)
{
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(
localized[0],
scrollDuration,
loopDuration,
holdDuration,
fontSize));
}
[YarnCommand("expression_memory_cut")]
public static void ExpressionMemoryCut()
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.CutMemoryAndGlitchImmediate();
}
[YarnCommand("expression_truth_snap")]
public static void ExpressionTruthSnap(float peripheral, float calmRadius)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SnapTruthImmediate(peripheral, calmRadius);
}
[YarnCommand("expression_glitch_pulse")]
public static void ExpressionGlitchPulse(float peak, float riseDuration, float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayGlitchPulse(peak, riseDuration, fallDuration);
}
/// <summary>
/// 火山「屏幕」绝对档位(非阻塞)。作用在屏幕材质,不是叠一层噪波。
/// 会清掉当前脉冲并把底压直接设成 <paramref name="level"/>;到位后一直保持。
/// 逐句积蓄情绪请改用 expression_screen_stress / expression_screen_pulse
/// 这条命令保留给"我就是要现在正好这个档位"的场合。
/// <<expression_screen_glitch 0.55 1.2>> … <<expression_screen_glitch 0 1.5>>
/// </summary>
[YarnCommand("expression_screen_glitch")]
public static void ExpressionScreenGlitch(
float level = 0.5f,
float duration = 1f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] expression_screen_glitch 找不到 ExpressionManager;已安全跳过。");
return;
}
ExpressionManager.SetScreenGlitchLevel(level, duration);
}
/// <summary>
/// 屏幕蓄压(非阻塞):往底压罐里加一口,只升不降,这就是"积蓄的情绪"。
/// 每次情绪推进加一次,不用记上次是多少档;有软上限,堆不满。
/// <<expression_screen_stress 0.15>>
/// </summary>
/// <param name="amount">这次加多少(建议 0.08~0.2</param>
/// <param name="duration">爬升到新底压的时间</param>
[YarnCommand("expression_screen_stress")]
public static void ExpressionScreenStress(float amount = 0.15f, float duration = 0.8f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] expression_screen_stress 找不到 ExpressionManager;已安全跳过。");
return;
}
ExpressionManager.AddScreenStress(amount, duration);
}
/// <summary>
/// 屏幕冲击(非阻塞):瞬间顶到「底压 + strength」,随后自动落回底压。
/// 一句狠话打一记;底压很低的开场也能打得很响,打完自己回到安静。
/// <<expression_screen_pulse 0.6>>
/// </summary>
/// <param name="strength">冲击高度(建议 0.3~0.8</param>
/// <param name="decay">落回底压的时间</param>
[YarnCommand("expression_screen_pulse")]
public static void ExpressionScreenPulse(float strength = 0.5f, float decay = 1.6f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] expression_screen_pulse 找不到 ExpressionManager;已安全跳过。");
return;
}
ExpressionManager.PulseScreenGlitch(strength, decay);
}
/// <summary>
/// 屏幕转红(非阻塞):荧光色从青绿推向愤怒红,LOG3 攻击段用。
/// 只换颜色不改故障强度,配合 expression_screen_pulse 打 punch。
/// <<expression_screen_rage 1 0.4>> … <<expression_screen_rage 0 1>>
/// </summary>
[YarnCommand("expression_screen_rage")]
public static void ExpressionScreenRage(float level = 1f, float duration = 0.5f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] expression_screen_rage 找不到 ExpressionManager;已安全跳过。");
return;
}
ExpressionManager.SetScreenRage(level, duration);
}
/// <summary>
/// 屏幕泄压(非阻塞):底压和脉冲一起放掉归零,情绪落幕时用。
/// <<expression_screen_release 1.2>>
/// </summary>
[YarnCommand("expression_screen_release")]
public static void ExpressionScreenRelease(float duration = 1.2f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError(
"[LanguageYarnCommand] expression_screen_release 找不到 ExpressionManager;已安全跳过。");
return;
}
ExpressionManager.ReleaseScreenStress(duration);
}
/// <summary>
/// 用现有目标粒子完整显示一个红色攻击词,并等待确定性的冲击动作结束。
/// 超过目标粒子数量时会安全跳过,不会截断。
/// <<expression_truth_attack "l10n.hs.exp.atk.why" 0.34 0.45>>
/// </summary>
[YarnCommand("expression_truth_attack")]
public static IEnumerator ExpressionTruthAttack(
string text,
float duration = 0.34f,
float impact = 0.5f)
{
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(localized[0], duration, impact));
}
/// <summary>
/// Keep an already resolved truth phrase in place while it gradually becomes unstable and red.
/// <<expression_truth_destabilize "l10n.hs.exp.log3.target" 1.6 0.13 22 "FF304D">>
/// </summary>
[YarnCommand("expression_truth_destabilize")]
public static IEnumerator ExpressionTruthDestabilize(
string text,
float duration = 1.6f,
float shakeAmplitude = 0.13f,
float shakeSpeed = 22f,
string colorHex = "FF304D")
{
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(
localized[0],
duration,
shakeAmplitude,
shakeSpeed,
colorHex));
}
/// <summary>
/// 将当前攻击词重组为固定青绿色真话,并等待稳定完成。
/// <<expression_truth_settle "l10n.hs.exp.final.win" 0.8>>
/// </summary>
[YarnCommand("expression_truth_settle")]
public static IEnumerator ExpressionTruthSettle(
string text,
float duration = 0.8f)
{
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(localized[0], duration));
}
[YarnCommand("expression_truth_blackout")]
public static IEnumerator ExpressionTruthBlackout(float duration, float alpha)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.HoldTruthBlackout(duration, alpha));
}
[YarnCommand("expression_lie_split")]
public static void ExpressionLieSplit(
float duration,
float distanceRatio,
float stretchX,
float squashY)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
{
presentation.SplitLieVisual(
duration,
distanceRatio,
stretchX,
squashY);
}
}
[YarnCommand("expression_truth_flash")]
public static void ExpressionTruthFlash(
float alpha,
float riseDuration,
float holdDuration,
float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
{
presentation.PlayTruthFlash(
alpha,
riseDuration,
holdDuration,
fallDuration);
}
}
[YarnCommand("expression_memory_brightness_pulse")]
public static void ExpressionMemoryBrightnessPulse(
float amount,
float riseDuration,
float fallDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayMemoryBrightnessPulse(amount, riseDuration, fallDuration);
}
[YarnCommand("expression_memory_tear")]
public static void ExpressionMemoryTear(float peak, float duration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayMemoryTear(peak, duration);
}
[YarnCommand("expression_face_dip")]
public static void ExpressionFaceDip(float duration, float ratio)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.PlayFaceDip(duration, ratio);
}
[YarnCommand("expression_truth_release")]
public static void ExpressionTruthRelease(float alphaDuration)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.ReleaseTruthCharacters(alphaDuration);
}
[YarnCommand("expression_truth_resolve")]
public static IEnumerator ExpressionTruthResolve(
float duration,
float peripheral,
float calmRadius)
{
if (!TryGetPresentation(out LogReleasePresentationController presentation))
yield break;
yield return ExpressionManager.StartCoroutine(
presentation.ResolveTruthCharacters(duration, peripheral, calmRadius));
}
[YarnCommand("expression_memory_end")]
public static IEnumerator ExpressionMemoryEnd()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
yield break;
}
yield return ExpressionManager.StartCoroutine(ExpressionManager.EndExpressionMemory());
}
[YarnCommand("expression_truth_impact")]
public static void ExpressionTruthImpact()
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
return;
}
ExpressionManager.ImpactExpressionTruth();
}
// /// <summary>
// /// Glitch 噪波过渡(指定起止值),等待完成后再继续下一步。
// /// </summary>
// [YarnCommand("glitch_transition")]
// public static IEnumerator GlitchTransition(float from, float to, float duration)
// {
// if (ExpressionManager == null)
// {
// UnityEngine.Debug.LogError("ExpressionManager 未找到!");
// yield break;
// }
// yield return ExpressionManager.StartCoroutine(ExpressionManager.TransitionGlitchAndWait(from, to, duration));
// }
}
}