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

986 lines
36 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 DG.Tweening;
using UnityEngine;
using AibisDream.FixSystem;
using AibisDream.MiniGame.Language;
using System.Collections.Generic;
using UnityEngine.Localization;
namespace AibisDream
{
/// <summary>
/// 表达系统顶层管理器(原 ExpressionManager,现改为语言粒子系统)
/// </summary>
public class ExpressionManager : MonoBehaviour
{
[Header("系统组件")]
[SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private GameObject expressionView;
[SerializeField] private LogReleasePresentationController logReleasePresentation;
[SerializeField] private ExpressionScreenPresentationController screenPresentation;
[SerializeField] private ExpressionContentCatalog contentCatalog;
[Header("Sprite 淡入淡出")]
[SerializeField] private SpriteRenderer screenOverlayRenderer;
[SerializeField] private SpriteRenderer volcanoOverlayRenderer;
[SerializeField, Range(0f, 1f)] private float spriteOverlayFadeInAlpha = 1f;
// 屏幕的发光归材质管(M_HuoshanScreenGlitch 的 _Emissive),这里只锁纯白不再叠亮度倍率。
// 曾经有过 screenBloomBrightness 字段,和 _Emissive 叠乘会让屏幕过曝——发光只能有一个真相源。
[Header("Glitch 噪波效果")]
[Tooltip("记忆/对白阶段的透明噪波叠层(glitch_transition)。expression_screen_glitch 已改走屏幕 Shader。")]
[SerializeField] private SpriteNoiseGlitchController glitchController;
[Header("屏幕 Glitch Shader")]
[Tooltip("仅在屏幕淡入 / expression_screen_glitch 时套用;小游戏阶段保持默认 Sprite 材质,避免进 VolFx 盖住粒子")]
[SerializeField] private Material screenGlitchMaterial;
// 观感全部在材质 M_HuoshanScreenGlitch 上调(只有 6 个宏观旋钮:Scale / ColorDamage /
// Warp / Chaos / Grit / SurgeRate)。代码这边只驱动 _GlitchIntensity 档位。
// 不要再把观感参数搬回 Inspector——两处都能改就等于没有真相源,上一版 40 个属性
// 分散在材质和组件两边,结果谁也调不动。
//
// 档位 = 底压 + 脉冲:底压(AddScreenStress)只升不降,是"积蓄的情绪";
// 脉冲(PulseScreenGlitch)瞬间打上去自动衰回底压,是"一句狠话"。
// SetScreenGlitchLevel 保留为绝对档位(会清掉脉冲),兼容旧 Yarn 脚本。
[Tooltip("蓄压底压的软上限;expression_screen_stress 只能加到这里,顶格瞬间留给 pulse 冲出去")]
[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 档位(01),由 expression_screen_glitch 持续保持。</summary>
public float ScreenGlitchLevel => screenGlitchIntensity;
private static readonly int ScreenGlitchIntensityId = Shader.PropertyToID("_GlitchIntensity");
private static readonly int ScreenRageId = Shader.PropertyToID("_Rage");
private Material screenDefaultMaterial;
private Material screenFallbackMaterial;
private Material screenGlitchMaterialInstance;
private Coroutine screenGlitchRampRoutine; // 底压过渡 / 泄压
private Coroutine screenGlitchPulseRoutine; // 脉冲衰减
private Coroutine screenRageRoutine; // 荧光转红
private float screenStressFloor; // 蓄压底压(当前值)
private float screenPulse; // 脉冲瞬时值
private float screenGlitchIntensity; // 实际写进材质的合成值
private float screenRage; // 荧光转红程度(0 青绿 / 1 愤怒红)
private bool screenGlitchMaterialActive;
void Start()
{
// 注册到系统字典
FixSystemCenter.SystemDic.Register(this);
// 查找并初始化表达视图
if (expressionView == null)
{
expressionView = transform.Find("Expression View")?.gameObject;
}
if (expressionView != null)
{
expressionView.SetActive(false);
}
// 查找粒子管理器
if (particleManager == null)
{
particleManager = GetComponentInChildren<LanguageParticleManager>(true);
}
if (glitchController == null)
{
glitchController = GetComponentInChildren<SpriteNoiseGlitchController>(true);
}
// glitch_transition 用的 noise:必须停在 Default,绝不能进 HuoshanScreen(否则会叠进 VolFx 盖住粒子)。
PrepareNoiseOverlayForGlitchTransition(false);
CacheScreenDefaultMaterial();
// 小游戏阶段绝不能提前套 Glitch 材质:其 SRPDefaultUnlit 会进 VolFx huoshan 合成并盖住粒子。
RestoreScreenDefaultMaterial();
if (logReleasePresentation == null)
{
logReleasePresentation = GetComponent<LogReleasePresentationController>();
}
if (screenPresentation == null)
{
screenPresentation = GetComponent<ExpressionScreenPresentationController>();
}
if (particleManager != null)
{
particleManager.SetPresentationController(logReleasePresentation);
particleManager.SetScreenPresentationController(screenPresentation);
}
}
/// <summary>
/// 打开表达视图(只显示美术背景,不启动粒子系统)
/// 先设置所有初始状态,再打开视图
/// </summary>
public void OpenView()
{
screenPresentation?.ResetImmediate();
// 先设置所有初始状态再打开
if (particleManager != null)
{
particleManager.SetInitialStatesBeforeOpen();
}
if (expressionView != null)
{
expressionView.SetActive(true);
}
}
/// <summary>
/// 启动粒子系统(在打开视图后调用,通常通过 Yarn 对话控制)
/// </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,
IReadOnlyList<string> defaultCharacterPool,
string completionNodeName = null,
int nonTargetParticleTotal = -1)
{
HideSpriteOverlaysForMinigame();
if (particleManager != null)
{
particleManager.SetPresentationController(logReleasePresentation);
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);
}
}
private void HideSpriteOverlaysForMinigame()
{
ResetScreenGlitchState();
// 粒子玩法必须回到默认 Sprite 材质,否则屏幕会进 VolFx 层把字盖掉。
RestoreScreenDefaultMaterial();
List<SpriteRenderer> renderers = CollectSpriteOverlayRenderers();
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
if (sr == null) continue;
DOTween.Kill(sr);
if (IsScreenOverlayRenderer(sr))
{
LockScreenOverlayRgb(sr, 0f);
}
else
{
Color color = sr.color;
color.a = 0f;
sr.color = color;
}
sr.enabled = false;
sr.gameObject.SetActive(false);
}
}
/// <summary>
/// 启动粒子系统(字符串数组版本,方便 Yarn 调用)
/// </summary>
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,
defaultCharacterPool,
completionNodeName,
nonTargetParticleTotal);
}
/// <summary>
/// 若火山释放 log 界面已淡出,则先淡入再返回(start_expression 时调用)
/// </summary>
/// <param name="duration">淡入时长(秒)</param>
public IEnumerator EnsureReleaseLogFadedIn(float duration = 1f)
{
if (particleManager == null) yield break;
yield return particleManager.FadeInReleaseLogIfNeeded(duration);
}
/// <summary>
/// 再次 <c>start_expression</c> 前淡出上一轮粒子与连线(若尚未初始化则立即结束)。
/// duration≤0 时使用 LanguageParticleManager 上的默认时长。
/// </summary>
public IEnumerator FadeOutParticlesBeforeNewRound(float duration = 0f)
{
if (particleManager == null) yield break;
yield return StartCoroutine(particleManager.FadeOutBeforeNewExpressionRound(duration));
}
public IEnumerator PreparePresentationForNextRound()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.PrepareForNextRound();
}
/// <summary>
/// 等待表达流程就绪(粒子字入场完成即可;panel 动效可并行)
/// </summary>
public IEnumerator WaitUntilExpressionFlowReady()
{
if (particleManager == null) yield break;
yield return particleManager.WaitUntilExpressionFlowReady();
}
internal IEnumerator PlayScreenDeepAndWait()
{
if (screenPresentation == null)
yield break;
yield return screenPresentation.PlayDeepAndWait();
}
internal bool HasScreenPresentation => screenPresentation != null;
/// <summary>
/// 从聚焦保持态触发稳定化(概率递增 → 文字锁定 → 排列)
/// </summary>
public void ResolveFocusSequence()
{
if (particleManager != null)
{
particleManager.ResolveFocusSequence();
}
}
/// <summary>
/// 触发聚焦稳定化并等待全流程完成
/// </summary>
public IEnumerator ResolveFocusAndWait()
{
if (particleManager == null) yield break;
particleManager.ResolveFocusSequence();
yield return particleManager.WaitUntilFocusFinished();
}
/// <summary>
/// 关闭表达系统
/// </summary>
public void CloseView()
{
ActiveExpressionLocale = null;
screenPresentation?.ResetImmediate();
ResetScreenGlitchState();
ApplyScreenGlitchProperties();
if (logReleasePresentation != null)
{
logReleasePresentation.ForceCleanupImmediate(true);
}
if (expressionView != null)
{
expressionView.SetActive(false);
}
if (particleManager != null)
{
particleManager.StopSystem();
}
}
/// <summary>
/// 重置系统(保持当前配置)
/// </summary>
public void ResetSystem(bool playEntranceEffect = true)
{
if (logReleasePresentation != null)
{
logReleasePresentation.ForceCleanupImmediate(true);
}
if (particleManager != null)
{
particleManager.ResetGame(playEntranceEffect);
}
}
/// <summary>
/// 淡入 Sprite 图片(屏幕、火山同步)
/// </summary>
/// <param name="duration">淡入时长(秒)</param>
public IEnumerator FadeInSpriteOverlay(float duration = 1f)
{
List<SpriteRenderer> renderers = CollectSpriteOverlayRenderers();
if (renderers.Count == 0) yield break;
float fadeDuration = Mathf.Max(0.01f, duration);
float targetAlpha = Mathf.Clamp01(spriteOverlayFadeInAlpha);
Sequence sequence = DOTween.Sequence().SetTarget(this);
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
DOTween.Kill(sr);
PrepareOverlayRendererForFade(sr, 0f);
sr.enabled = true;
sr.gameObject.SetActive(true);
sequence.Join(sr.DOFade(targetAlpha, fadeDuration).SetEase(Ease.OutQuad));
}
// 淡入演出屏幕时才启用 Glitch 材质;强度保持 0,外观与普通屏幕一致。
ActivateScreenGlitchMaterial();
ApplyScreenGlitchProperties();
yield return sequence.WaitForCompletion();
for (int i = 0; i < renderers.Count; i++)
LockScreenOverlayRgb(renderers[i], renderers[i] != null ? renderers[i].color.a : 0f);
}
/// <summary>
/// 淡出 Sprite 图片(屏幕、火山同步)
/// </summary>
/// <param name="duration">淡出时长(秒)</param>
public IEnumerator FadeOutSpriteOverlay(float duration = 1f)
{
List<SpriteRenderer> renderers = CollectSpriteOverlayRenderers();
if (renderers.Count == 0) yield break;
float fadeDuration = Mathf.Max(0.01f, duration);
// 底压是保持型的:屏幕淡出时一并泄掉,免得下一段淡入时带着上一段积蓄的情绪。
ReleaseScreenStress(fadeDuration);
Sequence sequence = DOTween.Sequence().SetTarget(this);
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
DOTween.Kill(sr);
LockScreenOverlayRgb(sr, sr.color.a);
sr.enabled = true;
sr.gameObject.SetActive(true);
sequence.Join(sr.DOFade(0f, fadeDuration).SetEase(Ease.InQuad));
}
yield return sequence.WaitForCompletion();
for (int i = 0; i < renderers.Count; i++)
{
SpriteRenderer sr = renderers[i];
if (sr == null) continue;
LockScreenOverlayRgb(sr, 0f);
sr.enabled = false;
sr.gameObject.SetActive(false);
}
}
private List<SpriteRenderer> CollectSpriteOverlayRenderers()
{
var result = new List<SpriteRenderer>();
if (screenOverlayRenderer != null)
result.Add(screenOverlayRenderer);
if (volcanoOverlayRenderer != null && volcanoOverlayRenderer != screenOverlayRenderer)
result.Add(volcanoOverlayRenderer);
return result;
}
private bool IsScreenOverlayRenderer(SpriteRenderer sr)
{
return sr != null && sr == screenOverlayRenderer;
}
private void PrepareOverlayRendererForFade(SpriteRenderer sr, float alpha)
{
if (sr == null) return;
if (IsScreenOverlayRenderer(sr))
{
LockScreenOverlayRgb(sr, alpha);
return;
}
Color color = sr.color;
color.a = alpha;
sr.color = color;
}
private void LockScreenOverlayRgb(SpriteRenderer sr, float alpha)
{
if (!IsScreenOverlayRenderer(sr)) return;
Color locked = Color.white;
locked.a = Mathf.Clamp01(alpha);
sr.color = locked;
}
private void CacheScreenDefaultMaterial()
{
if (screenOverlayRenderer == null || screenDefaultMaterial != null)
return;
var current = screenOverlayRenderer.sharedMaterial;
// 若场景里误挂了 Glitch 材质,仍回退到 URP 默认 Sprite,保证小游戏可见。
if (current != null && current.shader != null &&
current.shader.name == "AibisDream/HuoshanScreenGlitch")
{
screenDefaultMaterial = null;
return;
}
screenDefaultMaterial = current;
}
private void RestoreScreenDefaultMaterial()
{
if (screenOverlayRenderer == null)
return;
CacheScreenDefaultMaterial();
screenGlitchMaterialActive = false;
screenStressFloor = 0f;
screenPulse = 0f;
screenGlitchIntensity = 0f;
screenRage = 0f;
if (screenDefaultMaterial != null)
{
screenOverlayRenderer.sharedMaterial = screenDefaultMaterial;
return;
}
if (screenOverlayRenderer.sharedMaterial != null &&
screenOverlayRenderer.sharedMaterial.shader != null &&
screenOverlayRenderer.sharedMaterial.shader.name == "AibisDream/HuoshanScreenGlitch")
{
// 场景直接挂了 Glitch 材质时要换掉,否则它的 SRPDefaultUnlit 会进 VolFx 合成盖住粒子。
// 绝不能置 null——SpriteRenderer 的 material 为 null 会渲染成品红方块,
// 表现为 switch_fix_system_to Expression 后闪一下粉色。
screenOverlayRenderer.sharedMaterial = GetScreenFallbackMaterial();
}
}
/// <summary>
/// 场景没有提供干净的屏幕材质时的兜底:标准 Sprite 材质。
/// </summary>
private Material GetScreenFallbackMaterial()
{
if (screenFallbackMaterial != null)
return screenFallbackMaterial;
Shader spriteShader = Shader.Find("Sprites/Default");
if (spriteShader == null)
{
Debug.LogError("[ExpressionManager] 找不到 Sprites/Default;屏幕材质无法回退。", this);
return null;
}
screenFallbackMaterial = new Material(spriteShader) { name = "Screen Sprite Fallback" };
return screenFallbackMaterial;
}
private void ActivateScreenGlitchMaterial()
{
if (screenOverlayRenderer == null)
return;
CacheScreenDefaultMaterial();
if (screenGlitchMaterial == null)
{
Debug.LogError(
"[ExpressionManager] 未找到屏幕 Glitch 材质;请把 M_HuoshanScreenGlitch 赋给 screenGlitchMaterial。",
this);
return;
}
if (screenGlitchMaterialInstance == null)
{
screenGlitchMaterialInstance = new Material(screenGlitchMaterial)
{
name = screenGlitchMaterial.name + " (Screen Instance)"
};
}
screenOverlayRenderer.sharedMaterial = screenGlitchMaterialInstance;
screenGlitchMaterialActive = true;
ApplyScreenGlitchProperties();
}
private void ApplyScreenGlitchProperties()
{
if (!screenGlitchMaterialActive || screenGlitchMaterialInstance == null)
return;
// 只驱动强度档位与愤怒红;其余观感是材质的事。
screenGlitchMaterialInstance.SetFloat(ScreenGlitchIntensityId, screenGlitchIntensity);
screenGlitchMaterialInstance.SetFloat(ScreenRageId, screenRage);
}
private void SetScreenGlitchIntensity(float value)
{
screenGlitchIntensity = Mathf.Clamp01(value);
if (screenGlitchIntensity > 0f && !screenGlitchMaterialActive)
ActivateScreenGlitchMaterial();
ApplyScreenGlitchProperties();
}
/// <summary>
/// 恢复 glitch_transition 用的 noise 叠层到原始层级关系:
/// Default 层 + sortingOrder 10 + OpaqueBackground。
/// 不要对齐/改到 HuoshanScreen,否则会进 VolFx 合成叠在粒子上面。
/// </summary>
private void PrepareNoiseOverlayForGlitchTransition(bool activateForUse)
{
if (glitchController == null)
return;
const int defaultLayer = 0;
glitchController.gameObject.layer = defaultLayer;
var noiseRenderer = glitchController.GetComponent<SpriteRenderer>();
if (noiseRenderer != null)
{
noiseRenderer.sortingLayerID = 0;
noiseRenderer.sortingOrder = 10;
if (activateForUse)
noiseRenderer.enabled = true;
}
// 与场景原始配置一致:不透明黑底噪波(强度 0 时几乎全黑底,由上层 HuoshanScreen 内容盖住中心)。
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.OpaqueBackground);
if (activateForUse && !glitchController.gameObject.activeSelf)
glitchController.gameObject.SetActive(true);
}
/// <summary>
/// Glitch 噪波过渡:驱动 ExpressView/noiseSpriteNoiseGlitch),非屏幕 Shader。
/// </summary>
public void TransitionGlitch(float to, float duration = 0f)
{
if (glitchController == null) return;
PrepareNoiseOverlayForGlitchTransition(true);
glitchController.TransitionTo(to, duration);
}
/// <summary>
/// 把「屏幕」Sprite 的故障档位推到 <paramref name="level"/> 并保持住(非阻塞)。
/// 绝对档位语义:直接设定底压并清掉脉冲,兼容旧 Yarn 脚本。
/// 逐句积蓄情绪请改用 AddScreenStress / PulseScreenGlitch / ReleaseScreenStress。
/// </summary>
public void SetScreenGlitchLevel(float level, float duration)
{
float target = Mathf.Clamp01(level);
if (!PrepareScreenGlitch(target))
return;
StopScreenGlitchRoutines();
screenPulse = 0f;
float ramp = Mathf.Max(0f, duration);
if (ramp <= 0.02f || !isActiveAndEnabled)
{
screenStressFloor = target;
ApplyCombinedScreenGlitch();
return;
}
screenGlitchRampRoutine = StartCoroutine(RampScreenStressFloor(target, ramp));
}
/// <summary>
/// 蓄压(非阻塞):往底压罐里加 <paramref name="amount"/> 并一直保持,只升不降。
/// 上限 screenStressCap——顶格情绪不靠底压堆满,留给 PulseScreenGlitch 冲出去。
/// </summary>
public void AddScreenStress(float amount, float duration = 0.8f)
{
if (amount <= 0f || !PrepareScreenGlitch(1f))
return;
if (screenGlitchRampRoutine != null)
{
StopCoroutine(screenGlitchRampRoutine);
screenGlitchRampRoutine = null;
}
float target = Mathf.Max(screenStressFloor,
Mathf.Min(screenStressCap, screenStressFloor + amount));
float ramp = Mathf.Max(0f, duration);
if (ramp <= 0.02f || !isActiveAndEnabled)
{
screenStressFloor = target;
ApplyCombinedScreenGlitch();
return;
}
screenGlitchRampRoutine = StartCoroutine(RampScreenStressFloor(target, ramp));
}
/// <summary>
/// 冲击(非阻塞):瞬间顶到 底压+<paramref name="strength"/>(合成值上限 1),
/// 随后在 <paramref name="decayDuration"/> 内自动落回底压。
/// 连续冲击取 max,只会顶高不会互相打断。开场底压很低时也能打出响的。
/// </summary>
public void PulseScreenGlitch(float strength, float decayDuration = 1.6f)
{
if (strength <= 0f || !PrepareScreenGlitch(1f))
return;
screenPulse = Mathf.Max(screenPulse, Mathf.Clamp01(strength));
ApplyCombinedScreenGlitch();
if (screenGlitchPulseRoutine != null)
{
StopCoroutine(screenGlitchPulseRoutine);
screenGlitchPulseRoutine = null;
}
if (!isActiveAndEnabled)
{
screenPulse = 0f;
ApplyCombinedScreenGlitch();
return;
}
screenGlitchPulseRoutine = StartCoroutine(
DecayScreenPulse(Mathf.Max(0.05f, decayDuration)));
}
/// <summary>
/// 泄压(非阻塞):底压和脉冲一起在 <paramref name="duration"/> 内放掉归零。
/// </summary>
public void ReleaseScreenStress(float duration = 1.2f)
{
if (screenOverlayRenderer == null)
return;
if (!screenGlitchMaterialActive)
{
ResetScreenGlitchState();
return;
}
StopScreenGlitchRoutines();
float ramp = Mathf.Max(0f, duration);
if (ramp <= 0.02f || !isActiveAndEnabled)
{
screenStressFloor = 0f;
screenPulse = 0f;
ApplyCombinedScreenGlitch();
return;
}
screenGlitchRampRoutine = StartCoroutine(ReleaseScreenStressRoutine(ramp));
}
/// <summary>
/// 激活材质并确保屏幕可见;目标为 0 且材质未激活时直接清零返回 false。
/// </summary>
private bool PrepareScreenGlitch(float targetLevel)
{
if (screenOverlayRenderer == null)
return false;
if (targetLevel > 0f)
{
ActivateScreenGlitchMaterial();
if (!screenGlitchMaterialActive)
return false;
// 确保屏幕可见(否则材质在动但物体是关的)。
screenOverlayRenderer.enabled = true;
screenOverlayRenderer.gameObject.SetActive(true);
return true;
}
if (!screenGlitchMaterialActive)
{
screenStressFloor = 0f;
screenPulse = 0f;
screenGlitchIntensity = 0f;
screenRage = 0f;
return false;
}
return true;
}
/// <summary>
/// 攻击词的屏幕 punch:仅在红屏武装(screenRage &gt; 0)时生效,
/// 力度跟着这个词的 impact 走,衰减很短所以读作"闪一下"而不是"又坏了一档"。
/// 由 expression_truth_attack 自动调用——不要在 Yarn 里逐条手写 pulse,
/// 攻击列表会重调,两边手写必然脱节。
/// </summary>
public void PunchScreenForAttack(float impact)
{
if (screenRage <= 0.01f || !screenGlitchMaterialActive)
return;
PulseScreenGlitch(Mathf.Clamp(0.18f + impact * 0.34f, 0f, 0.6f) * screenRage, 0.28f);
}
/// <summary>
/// 把屏幕荧光从青绿推向愤怒红(非阻塞)。LOG3 攻击段用;<paramref name="level"/> 0 收回。
/// 只换颜色,不影响故障强度——强度仍归 stress/pulse 管。
/// 红屏武装期间,expression_truth_attack 会自动 punch 屏幕。
/// </summary>
public void SetScreenRage(float level, float duration = 0.5f)
{
if (screenOverlayRenderer == null)
return;
float target = Mathf.Clamp01(level);
if (target > 0f && !PrepareScreenGlitch(1f))
return;
if (screenRageRoutine != null)
{
StopCoroutine(screenRageRoutine);
screenRageRoutine = null;
}
float ramp = Mathf.Max(0f, duration);
if (ramp <= 0.02f || !isActiveAndEnabled)
{
screenRage = target;
ApplyScreenGlitchProperties();
return;
}
screenRageRoutine = StartCoroutine(RampScreenRage(target, ramp));
}
private IEnumerator RampScreenRage(float target, float duration)
{
float start = screenRage;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
screenRage = Mathf.Lerp(start, target, t);
ApplyScreenGlitchProperties();
yield return null;
}
screenRage = target;
ApplyScreenGlitchProperties();
screenRageRoutine = null;
}
private void StopScreenGlitchRoutines()
{
if (screenGlitchRampRoutine != null)
{
StopCoroutine(screenGlitchRampRoutine);
screenGlitchRampRoutine = null;
}
if (screenGlitchPulseRoutine != null)
{
StopCoroutine(screenGlitchPulseRoutine);
screenGlitchPulseRoutine = null;
}
if (screenRageRoutine != null)
{
StopCoroutine(screenRageRoutine);
screenRageRoutine = null;
}
}
private void ResetScreenGlitchState()
{
StopScreenGlitchRoutines();
screenStressFloor = 0f;
screenPulse = 0f;
screenGlitchIntensity = 0f;
screenRage = 0f;
}
private void ApplyCombinedScreenGlitch()
{
SetScreenGlitchIntensity(screenStressFloor + screenPulse);
}
private IEnumerator RampScreenStressFloor(float target, float duration)
{
float start = screenStressFloor;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
screenStressFloor = Mathf.Lerp(start, target, t);
ApplyCombinedScreenGlitch();
yield return null;
}
screenStressFloor = target;
ApplyCombinedScreenGlitch();
screenGlitchRampRoutine = null;
}
private IEnumerator DecayScreenPulse(float duration)
{
float start = screenPulse;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
screenPulse = Mathf.Lerp(start, 0f, t);
ApplyCombinedScreenGlitch();
yield return null;
}
screenPulse = 0f;
ApplyCombinedScreenGlitch();
screenGlitchPulseRoutine = null;
}
private IEnumerator ReleaseScreenStressRoutine(float duration)
{
float startFloor = screenStressFloor;
float startPulse = screenPulse;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
screenStressFloor = Mathf.Lerp(startFloor, 0f, t);
screenPulse = Mathf.Lerp(startPulse, 0f, t);
ApplyCombinedScreenGlitch();
yield return null;
}
screenStressFloor = 0f;
screenPulse = 0f;
ApplyCombinedScreenGlitch();
screenGlitchRampRoutine = null;
}
/// <summary>
/// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to。
/// </summary>
public IEnumerator TransitionGlitchAndWait(float to, float duration = 0f)
{
if (glitchController == null) yield break;
PrepareNoiseOverlayForGlitchTransition(true);
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
/// <summary>
/// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to(from 忽略,保持丝滑连续)。
/// </summary>
public IEnumerator TransitionGlitchAndWait(float from, float to, float duration)
{
if (glitchController == null) yield break;
PrepareNoiseOverlayForGlitchTransition(true);
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
public IEnumerator BeginExpressionMemory(
string memoryKey,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
bool preserveGlitch = false,
float targetOpacity = 1f)
{
if (logReleasePresentation == null)
{
Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;记忆命令安全结束。");
yield break;
}
yield return logReleasePresentation.BeginMemory(
memoryKey,
fadeDuration,
pushSpeed,
horizontalSpeed,
preserveGlitch,
targetOpacity);
}
public IEnumerator EndExpressionMemory()
{
if (logReleasePresentation == null) yield break;
yield return logReleasePresentation.EndMemory();
}
public void ImpactExpressionTruth()
{
logReleasePresentation?.TruthImpact();
}
private void OnDisable()
{
ActiveExpressionLocale = null;
ResetScreenGlitchState();
ApplyScreenGlitchProperties();
if (logReleasePresentation != null)
logReleasePresentation.ForceCleanupImmediate(true);
}
private void OnDestroy()
{
if (screenGlitchMaterialInstance != null)
{
if (Application.isPlaying)
Destroy(screenGlitchMaterialInstance);
else
DestroyImmediate(screenGlitchMaterialInstance);
screenGlitchMaterialInstance = null;
}
if (screenFallbackMaterial != null)
{
if (Application.isPlaying)
Destroy(screenFallbackMaterial);
else
DestroyImmediate(screenFallbackMaterial);
screenFallbackMaterial = null;
}
}
}
}