using System.Collections; using DG.Tweening; using UnityEngine; using AibisDream.FixSystem; using AibisDream.MiniGame.Language; using System.Collections.Generic; using UnityEngine.Localization; namespace AibisDream { /// /// 表达系统顶层管理器(原 ExpressionManager,现改为语言粒子系统) /// 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; [SerializeField] private ExpressionParticleLanguageProfile particleLanguageProfile; [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 ExpressionParticleLanguageProfile ParticleLanguageProfile => particleLanguageProfile; public Locale ActiveExpressionLocale { get; private set; } public Locale GetExpressionLocale(Locale fallback) { return ActiveExpressionLocale != null ? ActiveExpressionLocale : fallback; } public void SetActiveExpressionLocale(Locale locale) { ActiveExpressionLocale = locale; } /// 当前屏幕 Glitch 档位(0–1),由 expression_screen_glitch 持续保持。 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(true); } if (glitchController == null) { glitchController = GetComponentInChildren(true); } // glitch_transition 用的 noise:必须停在 Default,绝不能进 HuoshanScreen(否则会叠进 VolFx 盖住粒子)。 PrepareNoiseOverlayForGlitchTransition(false); CacheScreenDefaultMaterial(); // 小游戏阶段绝不能提前套 Glitch 材质:其 SRPDefaultUnlit 会进 VolFx huoshan 合成并盖住粒子。 RestoreScreenDefaultMaterial(); if (logReleasePresentation == null) { logReleasePresentation = GetComponent(); } if (screenPresentation == null) { screenPresentation = GetComponent(); } if (particleManager != null) { particleManager.SetPresentationController(logReleasePresentation); particleManager.SetScreenPresentationController(screenPresentation); } } /// /// 打开表达视图(只显示美术背景,不启动粒子系统) /// 先设置所有初始状态,再打开视图 /// public void OpenView() { screenPresentation?.ResetImmediate(); // 先设置所有初始状态再打开 if (particleManager != null) { particleManager.SetInitialStatesBeforeOpen(); } if (expressionView != null) { expressionView.SetActive(true); } } /// /// 启动粒子系统(在打开视图后调用,通常通过 Yarn 对话控制) /// /// 当前 Locale 已完成粒子单位解析的轮次快照 /// 完成时触发的对话节点名称(可选) /// 轮次配置中的基础非目标粒子数 public void StartSystem( ExpressionRoundTextSnapshot snapshot, string completionNodeName = null, int baseNonTargetParticleCount = -1) { HideSpriteOverlaysForMinigame(); if (particleManager != null) { particleManager.SetPresentationController(logReleasePresentation); if (snapshot == null || snapshot.TargetUnits == null || snapshot.TargetUnits.Count == 0 || snapshot.DefaultPoolUnits == null || snapshot.DefaultPoolUnits.Count == 0 || snapshot.InterferencePoolUnits == null || snapshot.InterferencePoolUnits.Count == 0) { Debug.LogError( "[ExpressionManager] 轮次粒子单位快照无效;系统未启动。"); return; } particleManager.InitializeSystem( snapshot, completionNodeName, baseNonTargetParticleCount); } } private void HideSpriteOverlaysForMinigame() { ResetScreenGlitchState(); // 粒子玩法必须回到默认 Sprite 材质,否则屏幕会进 VolFx 层把字盖掉。 RestoreScreenDefaultMaterial(); List 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); } } /// /// 若火山释放 log 界面已淡出,则先淡入再返回(start_expression 时调用) /// /// 淡入时长(秒) public IEnumerator EnsureReleaseLogFadedIn(float duration = 1f) { if (particleManager == null) yield break; yield return particleManager.FadeInReleaseLogIfNeeded(duration); } /// /// 再次 start_expression 前淡出上一轮粒子与连线(若尚未初始化则立即结束)。 /// duration≤0 时使用 LanguageParticleManager 上的默认时长。 /// 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(); } /// /// 等待表达流程就绪(粒子字入场完成即可;panel 动效可并行) /// 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; /// /// 从聚焦保持态触发稳定化(概率递增 → 文字锁定 → 排列) /// public void ResolveFocusSequence() { if (particleManager != null) { particleManager.ResolveFocusSequence(); } } /// /// 触发聚焦稳定化并等待全流程完成 /// public IEnumerator ResolveFocusAndWait() { if (particleManager == null) yield break; particleManager.ResolveFocusSequence(); yield return particleManager.WaitUntilFocusFinished(); } /// /// 关闭表达系统 /// 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(); } } /// /// 重置系统(保持当前配置) /// public void ResetSystem(bool playEntranceEffect = true) { if (logReleasePresentation != null) { logReleasePresentation.ForceCleanupImmediate(true); } if (particleManager != null) { particleManager.ResetGame(playEntranceEffect); } } /// /// 淡入 Sprite 图片(屏幕、火山同步) /// /// 淡入时长(秒) public IEnumerator FadeInSpriteOverlay(float duration = 1f) { List 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); } /// /// 淡出 Sprite 图片(屏幕、火山同步) /// /// 淡出时长(秒) public IEnumerator FadeOutSpriteOverlay(float duration = 1f) { List 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 CollectSpriteOverlayRenderers() { var result = new List(); 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(); } } /// /// 场景没有提供干净的屏幕材质时的兜底:标准 Sprite 材质。 /// 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(); } /// /// 恢复 glitch_transition 用的 noise 叠层到原始层级关系: /// Default 层 + sortingOrder 10 + OpaqueBackground。 /// 不要对齐/改到 HuoshanScreen,否则会进 VolFx 合成叠在粒子上面。 /// private void PrepareNoiseOverlayForGlitchTransition(bool activateForUse) { if (glitchController == null) return; const int defaultLayer = 0; glitchController.gameObject.layer = defaultLayer; var noiseRenderer = glitchController.GetComponent(); 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); } /// /// Glitch 噪波过渡:驱动 ExpressView/noise(SpriteNoiseGlitch),非屏幕 Shader。 /// public void TransitionGlitch(float to, float duration = 0f) { if (glitchController == null) return; PrepareNoiseOverlayForGlitchTransition(true); glitchController.TransitionTo(to, duration); } /// /// 把「屏幕」Sprite 的故障档位推到 并保持住(非阻塞)。 /// 绝对档位语义:直接设定底压并清掉脉冲,兼容旧 Yarn 脚本。 /// 逐句积蓄情绪请改用 AddScreenStress / PulseScreenGlitch / ReleaseScreenStress。 /// 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)); } /// /// 蓄压(非阻塞):往底压罐里加 并一直保持,只升不降。 /// 上限 screenStressCap——顶格情绪不靠底压堆满,留给 PulseScreenGlitch 冲出去。 /// 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)); } /// /// 冲击(非阻塞):瞬间顶到 底压+(合成值上限 1), /// 随后在 内自动落回底压。 /// 连续冲击取 max,只会顶高不会互相打断。开场底压很低时也能打出响的。 /// 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))); } /// /// 泄压(非阻塞):底压和脉冲一起在 内放掉归零。 /// 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)); } /// /// 激活材质并确保屏幕可见;目标为 0 且材质未激活时直接清零返回 false。 /// 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; } /// /// 攻击词的屏幕 punch:仅在红屏武装(screenRage > 0)时生效, /// 力度跟着这个词的 impact 走,衰减很短所以读作"闪一下"而不是"又坏了一档"。 /// 由 expression_truth_attack 自动调用——不要在 Yarn 里逐条手写 pulse, /// 攻击列表会重调,两边手写必然脱节。 /// 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); } /// /// 把屏幕荧光从青绿推向愤怒红(非阻塞)。LOG3 攻击段用; 0 收回。 /// 只换颜色,不影响故障强度——强度仍归 stress/pulse 管。 /// 红屏武装期间,expression_truth_attack 会自动 punch 屏幕。 /// 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; } /// /// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to。 /// public IEnumerator TransitionGlitchAndWait(float to, float duration = 0f) { if (glitchController == null) yield break; PrepareNoiseOverlayForGlitchTransition(true); yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration)); } /// /// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to(from 忽略,保持丝滑连续)。 /// 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; } } } }