using System; using System.Collections; using System.Collections.Generic; using System.Text; using DG.Tweening; using TMPro; using UnityEngine; namespace AibisDream.MiniGame.Language { /// /// Owns the post-output performance for Huoshan's three blocked logs. All memory /// distortion lives on the HuoshanMemory material; face / dialogue stay clean. /// public sealed class LogReleasePresentationController : MonoBehaviour { public enum PresentationState { Idle, LogOperation, FaceEntering, Memory, LieHolding, LieCracking, TruthResolving, TruthHolding, Returning } private enum MemoryKind { Office, Sunset, PrivateOffice } private readonly struct Preset { public readonly string Name; public readonly float BaseDistortion; public Preset( string name, float baseDistortion) { Name = name; BaseDistortion = baseDistortion; } } 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; [SerializeField] private SpriteRenderer faceRenderer; [SerializeField] private Sprite errorFaceSprite; [Tooltip("可选。记忆演出不再驱动 noise;仅在进入/退出时清零遗留 Intensity,避免 glitch_transition 残留。")] [SerializeField] private SpriteNoiseGlitchController glitchController; [SerializeField] private RectTransform expressionScreenMaskRect; [SerializeField] private TMP_FontAsset screenFont; [Header("Memory Assets")] [SerializeField] private Sprite officeMemory; [SerializeField] private Sprite sunsetMemory; [SerializeField] private Sprite privateOfficeMemory; [SerializeField] private Material memoryMaterial; [Header("Memory Simulation")] [Tooltip("三档演出预设下记忆材质的轻微失真强度;随记忆透明度一起浮现。")] [SerializeField, Range(0f, 1f)] private float simGlitchLight = 0.28f; [SerializeField, Range(0f, 1f)] private float simGlitchMedium = 0.36f; [SerializeField, Range(0f, 1f)] private float simGlitchHeavy = 0.44f; [Header("Timing")] [SerializeField, Min(0.01f)] private float faceFadeDuration = 0.45f; [SerializeField, Min(0.01f)] private float memoryFadeDuration = 0.55f; [SerializeField, Min(0.01f)] private float memoryExitDuration = 0.45f; [SerializeField, Min(0f)] private float faceDipRatio = 0.015f; [Header("Screen Styling")] [SerializeField] private Color lieColor = new Color(0.66f, 0.94f, 1f, 1f); [SerializeField] private Color truthWarmColor = new Color(1f, 1f, 1f, 1f); [Tooltip("爆发段真话闪现与老虎机真话字符的暖色;需与谎话的冷白明显区分。")] [SerializeField] private Color truthAccentColor = new Color(1f, 0.84f, 0.6f, 1f); [Tooltip("结尾真假闪切时真心话帧的屏幕底色(电视头背景)。")] [SerializeField] private Color truthBackdropColor = new Color(0.30f, 0.20f, 0.10f, 0.78f); [Tooltip("结尾真假闪切时谎话帧的屏幕底色。")] [SerializeField] private Color lieBackdropColor = new Color(0.02f, 0.05f, 0.09f, 0.82f); [Tooltip("梳理完成后目标粒子与真话字的颜色;玩法阶段仍用粒子管理器原色。")] [SerializeField] private Color presentationResolvedColor = Color.white; [Header("Truth Attack")] [Tooltip("攻击词命中时覆盖记忆层的瞬时怒火色。使用深红而非粉红,避免与攻击文字争夺可读性。")] [SerializeField] private Color truthAttackFlashColor = new Color(0.48f, 0.01f, 0.025f, 1f); [Tooltip("攻击强度从 0 到 1 时,整屏红色冲击的透明度范围。")] [SerializeField] private Vector2 truthAttackFlashAlpha = new Vector2(0.14f, 0.40f); [SerializeField] private int screenGlowSortingOrder = 8; [SerializeField] private int screenTextSortingOrder = 18; private PresentationState state = PresentationState.Idle; private Preset currentPreset = LightPreset; private MemoryKind currentMemoryKind = MemoryKind.Office; private string currentLieKeyword = string.Empty; private Transform runtimeRoot; private SpriteRenderer memoryRendererA; private SpriteRenderer memoryRendererB; private SpriteRenderer activeMemoryRenderer; private SpriteRenderer scanlineRenderer; private SpriteRenderer screenDimRenderer; private SpriteMask screenSpriteMask; private TextMeshPro lieText; private TextMeshPro lieGhostText; private TextMeshPro actorFlashText; private TextMeshPro actorFlashChromaRed; private TextMeshPro actorFlashChromaCyan; private TextMeshPro truthFlashText; private TMPRectClipper truthFlashClipper; private bool truthFlashActive; private SpriteRenderer flickerBackdropRenderer; private Coroutine truthLieFlickerRoutine; private string truthWarmColorHex; private TMPRectClipper lieClipper; private TMPRectClipper lieGhostClipper; private TMPRectClipper actorFlashClipper; private TMPRectClipper actorFlashChromaRedClipper; private TMPRectClipper actorFlashChromaCyanClipper; private readonly List actorScrollTexts = new List(); private readonly List actorScrollClippers = new List(); private bool actorFlashActive; private int actorFlashCount; private Coroutine actorOverdriveRoutine; private Texture2D runtimeWhiteTexture; private Sprite runtimeWhiteSprite; private MaterialPropertyBlock memoryBlockA; private MaterialPropertyBlock memoryBlockB; private float memoryOpacityA; private float memoryOpacityB; private float saturation = 1f; private float brightness = 1f; private float contrast = 1f; private float vignette; private float noise; private float simGlitch; private float memoryBaseSimGlitch; private float horizontalTear; private float memoryImpact; private float memoryDamage; private float memoryOverdrive; private float memoryTearSeed; private Color memoryTint = Color.white; private Vector2 calmCenter = new Vector2(0.5f, 0.5f); private float calmRadius; private float calmFeather = 0.18f; private float effectStrength; private float memoryPushSpeed; private float memoryPushProgress; private float memoryZoom = 1f; private Vector3 memoryBasePosition; private Vector3 memoryBaseScale; private Vector3 faceBasePosition; private Sprite normalFaceSprite; private bool runtimeObjectsReady; private Coroutine lieShuffleRoutine; private Coroutine faceErrorRoutine; private static readonly int SaturationId = Shader.PropertyToID("_Saturation"); private static readonly int BrightnessId = Shader.PropertyToID("_Brightness"); private static readonly int ContrastId = Shader.PropertyToID("_Contrast"); private static readonly int VignetteId = Shader.PropertyToID("_Vignette"); private static readonly int NoiseId = Shader.PropertyToID("_Noise"); private static readonly int SimGlitchId = Shader.PropertyToID("_SimGlitch"); private static readonly int HorizontalTearId = Shader.PropertyToID("_HorizontalTear"); private static readonly int MemoryImpactId = Shader.PropertyToID("_Impact"); private static readonly int MemoryDamageId = Shader.PropertyToID("_Damage"); private static readonly int MemoryOverdriveId = Shader.PropertyToID("_Overdrive"); private static readonly int MemoryTearSeedId = Shader.PropertyToID("_TearSeed"); private static readonly int ColorTintId = Shader.PropertyToID("_ColorTint"); private static readonly int CalmCenterId = Shader.PropertyToID("_CalmCenter"); private static readonly int CalmRadiusId = Shader.PropertyToID("_CalmRadius"); private static readonly int CalmFeatherId = Shader.PropertyToID("_CalmFeather"); private static readonly int EffectStrengthId = Shader.PropertyToID("_EffectStrength"); private static readonly int OpacityId = Shader.PropertyToID("_Opacity"); public PresentationState State => state; private void Awake() { EnsureRuntimeObjects(); faceBasePosition = faceRenderer != null ? faceRenderer.transform.position : Vector3.zero; normalFaceSprite = faceRenderer != null ? faceRenderer.sprite : null; ForceCleanupImmediate(true); } private void Update() { if (!runtimeObjectsReady || activeMemoryRenderer == null || !activeMemoryRenderer.enabled) return; memoryPushProgress += Time.deltaTime * memoryPushSpeed; activeMemoryRenderer.transform.localScale = memoryBaseScale * (1f + memoryPushProgress) * memoryZoom; activeMemoryRenderer.transform.position = memoryBasePosition; ApplyMemoryBlocks(); } public IEnumerator PrepareForNextRound() { EnsureRuntimeObjects(); bool anythingVisible = (faceRenderer != null && faceRenderer.gameObject.activeSelf && faceRenderer.color.a > 0.001f) || memoryOpacityA > 0.001f || memoryOpacityB > 0.001f || (lieText != null && lieText.gameObject.activeSelf) || (actorFlashText != null && actorFlashText.gameObject.activeSelf) || (truthFlashText != null && truthFlashText.gameObject.activeSelf) || actorScrollTexts.Exists(text => text != null && text.gameObject.activeSelf); DOTween.Kill(this); StopAllCoroutines(); state = PresentationState.Returning; if (!anythingVisible) { ForceCleanupImmediate(true); state = PresentationState.LogOperation; yield break; } const float duration = 0.35f; TweenMemoryOpacity(memoryRendererA, 0f, duration); TweenMemoryOpacity(memoryRendererB, 0f, duration); FadeText(lieText, 0f, duration); FadeText(lieGhostText, 0f, duration); FadeText(actorFlashText, 0f, duration); FadeText(actorFlashChromaRed, 0f, duration); FadeText(actorFlashChromaCyan, 0f, duration); FadeText(truthFlashText, 0f, duration); foreach (TextMeshPro text in actorScrollTexts) FadeText(text, 0f, duration); if (faceRenderer != null && faceRenderer.gameObject.activeSelf) faceRenderer.DOFade(0f, duration).SetEase(Ease.OutQuad).SetTarget(this); yield return new WaitForSeconds(duration); ForceCleanupImmediate(true); state = PresentationState.LogOperation; } public IEnumerator BeginFaceEntry(float duration = -1f) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!ExpectState( nameof(BeginFaceEntry), PresentationState.Idle, PresentationState.LogOperation, PresentationState.Returning)) { yield break; } state = PresentationState.FaceEntering; // 梳理完成起:目标字切到白色(并取消加粗),玩法阶段颜色不受影响。 particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor); SilenceNoiseOverlayIfPresent(); if (faceRenderer == null) { Debug.LogError("[LogReleasePresentation] 火山脸 SpriteRenderer 未配置;跳过脸淡入,避免阻塞 Yarn。"); state = PresentationState.Memory; yield break; } faceRenderer.transform.position = faceBasePosition; faceRenderer.enabled = true; faceRenderer.gameObject.SetActive(true); Color faceColor = faceRenderer.color; faceColor.a = 0f; faceRenderer.color = faceColor; float fadeDuration = duration > 0f ? Mathf.Max(0.01f, duration) : faceFadeDuration; yield return faceRenderer.DOFade(1f, fadeDuration) .SetEase(Ease.OutQuad) .SetTarget(this) .WaitForCompletion(); } public IEnumerator BeginMemory( string memoryKey, float fadeDuration = -1f, float pushSpeed = -1f, float horizontalSpeed = -1f, bool preserveGlitch = false, float targetOpacity = 1f) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!TryResolveMemory(memoryKey, out Sprite memorySprite, out MemoryKind kind)) { Debug.LogError($"[LogReleasePresentation] 未知记忆图片“{memoryKey}”;命令安全结束。"); yield break; } Preset preset = GetPresetForMemory(kind); if (!ExpectState( nameof(BeginMemory), PresentationState.Idle, PresentationState.LogOperation, PresentationState.FaceEntering, PresentationState.Memory, PresentationState.TruthHolding)) { yield break; } currentPreset = preset; currentMemoryKind = kind; _ = horizontalSpeed; actorFlashCount = 0; memoryImpact = 0f; memoryDamage = 0f; memoryOverdrive = 0f; memoryTearSeed = 0f; state = PresentationState.Memory; // 兜底:跳过 face_fade_in 直进记忆时,目标字仍切到演出白。 particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor); // preserveGlitch 已弃用:进记忆后失真只走 HuoshanMemory 材质,不再保留 noise 叠层。 _ = preserveGlitch; SpriteRenderer incoming = activeMemoryRenderer == memoryRendererA ? memoryRendererB : memoryRendererA; SpriteRenderer outgoing = activeMemoryRenderer; incoming.sprite = memorySprite; incoming.enabled = true; incoming.gameObject.SetActive(true); FitMemoryToView(incoming); activeMemoryRenderer = incoming; ConfigureMemoryLook(kind, preset); memoryBasePosition = incoming.transform.position; memoryBaseScale = incoming.transform.localScale; memoryPushSpeed = pushSpeed >= 0f ? pushSpeed : kind == MemoryKind.PrivateOffice ? 0.040f : 0.022f; memoryPushProgress = 0f; memoryZoom = 1f; float resolvedFadeDuration = fadeDuration > 0f ? Mathf.Max(0.01f, fadeDuration) : memoryFadeDuration; // 进记忆前的 glitch_transition noise → 透明叠层后淡出; // 记忆材质 _SimGlitch / Analog 随透明度一起浮现,完成交接。 ReleaseNoiseOverlayIntoMemory(resolvedFadeDuration); SetMemoryOpacity(incoming, 0f); TweenMemoryOpacity( incoming, Mathf.Clamp01(targetOpacity), resolvedFadeDuration, Ease.Linear); if (outgoing != null && outgoing != incoming) TweenMemoryOpacity(outgoing, 0f, resolvedFadeDuration); yield return new WaitForSeconds(resolvedFadeDuration); } /// 把当前记忆透明度推进到目标值(非等待),用于随对白逐段显影。 public void SetActiveMemoryOpacity(float target, float duration) { EnsureRuntimeObjects(); if (activeMemoryRenderer == null || !activeMemoryRenderer.enabled) return; TweenMemoryOpacity( activeMemoryRenderer, Mathf.Clamp01(target), Mathf.Max(0.01f, duration)); } public void PrepareLieVisuals(float freezeDuration, float noiseTarget) { EnsureRuntimeObjects(); if (!ExpectState(nameof(PrepareLieVisuals), PresentationState.Memory)) return; state = PresentationState.LieHolding; DOTween.Kill(this); float duration = Mathf.Max(0.01f, freezeDuration); // 谎话期间把模拟感压到目标值:记忆被伪装得"过分干净"。 float resolved = Mathf.Clamp01(noiseTarget); memoryBaseSimGlitch = resolved; DOTween.To(() => simGlitch, value => simGlitch = value, resolved, duration) .SetEase(Ease.OutQuad) .SetTarget(this); } /// 调节记忆材质的轻微失真扰动(非等待)。 public void SetSimulationGlitch(float target, float duration) { EnsureRuntimeObjects(); float resolved = Mathf.Clamp01(target); memoryBaseSimGlitch = resolved; DOTween.To(() => simGlitch, value => simGlitch = value, resolved, Mathf.Max(0.01f, duration)) .SetEase(Ease.OutQuad) .SetTarget(this); } public void SqueezeTruthCharacters(float duration, float jitter, float alpha) { if (!ExpectState( nameof(SqueezeTruthCharacters), PresentationState.LieHolding, PresentationState.LieCracking)) return; particleManager?.SetTargetParticlesAlpha(Mathf.Clamp01(alpha), Mathf.Max(0.01f, duration)); particleManager?.BeginLieEdgeCompression( Mathf.Max(0.01f, duration), Mathf.Max(0f, jitter)); } public void ShowLieKeyword(string keyword, float duration, float characterSpacing) { EnsureRuntimeObjects(); if (!ExpectState( nameof(ShowLieKeyword), PresentationState.LieHolding, PresentationState.LieCracking)) return; currentLieKeyword = keyword ?? string.Empty; PositionScreenText(); SetTextActive(lieText, currentLieKeyword, lieColor); lieText.characterSpacing = -26f; lieText.transform.localScale = new Vector3(1.35f, 0.62f, 1f); SetTextAlpha(lieText, 0f); float tweenDuration = Mathf.Max(0.01f, duration); DOTween.To( () => lieText.characterSpacing, value => lieText.characterSpacing = value, characterSpacing, tweenDuration) .SetEase(Ease.OutBack) .SetTarget(this); lieText.transform.DOScale(Vector3.one, tweenDuration) .SetEase(Ease.OutBack) .SetTarget(this); FadeText(lieText, 1f, Mathf.Min(0.14f, tweenDuration)); if (lieGhostText != null) lieGhostText.gameObject.SetActive(false); } public void PlayScreenScanline(float duration, bool stalled) { EnsureRuntimeObjects(); if (!ExpectState( nameof(PlayScreenScanline), PresentationState.LieHolding, PresentationState.LieCracking)) return; PlayScanline(Mathf.Max(0.01f, duration), stalled); } public void BeginLieCracking(float lieAlpha) { if (!ExpectState( nameof(BeginLieCracking), PresentationState.LieHolding, PresentationState.LieCracking)) return; state = PresentationState.LieCracking; if (lieText == null || !lieText.gameObject.activeSelf) return; Color dimmed = lieText.color; dimmed.a = Mathf.Clamp01(lieAlpha); lieText.color = dimmed; } public void PlayScreenDim(float alpha, float riseDuration, float fallDuration) { EnsureRuntimeObjects(); if (!ExpectState( nameof(PlayScreenDim), PresentationState.LieHolding, PresentationState.LieCracking)) return; PulseScreenDim(alpha, riseDuration, fallDuration); } public void PlayMemoryJolt(float offset, float duration, float tear) { EnsureRuntimeObjects(); if (!ExpectState( nameof(PlayMemoryJolt), PresentationState.Memory, PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthResolving)) return; horizontalTear = Mathf.Max(horizontalTear, Mathf.Clamp01(tear)); if (activeMemoryRenderer == null) return; Transform target = activeMemoryRenderer.transform; target.DOKill(); Vector3 origin = target.position; float totalDuration = Mathf.Max(0.02f, duration); Sequence sequence = DOTween.Sequence().SetTarget(this); sequence.Append(target.DOMoveX(origin.x + offset, totalDuration * 0.35f).SetEase(Ease.OutQuad)); sequence.Append(target.DOMoveX(origin.x, totalDuration * 0.65f).SetEase(Ease.InOutQuad)); } public IEnumerator LeakTruthFragment(string fragment, float duration) { if (!ExpectState( nameof(LeakTruthFragment), PresentationState.LieHolding, PresentationState.LieCracking)) yield break; string resolvedFragment = fragment ?? string.Empty; yield return ShuffleLieCharactersRoutine( resolvedFragment, Mathf.Max(0.01f, duration), 0.045f); } public void StartLieCharacterShuffle(string characterPool, float duration, float interval) { if (!ExpectState( nameof(StartLieCharacterShuffle), PresentationState.LieHolding, PresentationState.LieCracking)) return; StopLieShuffleImmediate(false); lieShuffleRoutine = StartCoroutine(ShuffleLieCharactersRoutine( characterPool, Mathf.Max(0.01f, duration), Mathf.Max(0.02f, interval))); } public void CutMemoryAndGlitchImmediate() { EnsureRuntimeObjects(); if (!ExpectState( nameof(CutMemoryAndGlitchImmediate), PresentationState.Memory, PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthHolding)) return; DOTween.Kill(this); StopActorOverdriveImmediate(); memoryPushSpeed = 0f; memoryPushProgress = 0f; memoryZoom = 1f; memoryOpacityA = 0f; memoryOpacityB = 0f; horizontalTear = 0f; noise = 0f; simGlitch = 0f; effectStrength = 0f; memoryImpact = 0f; memoryDamage = 0f; memoryOverdrive = 0f; calmRadius = 0f; DisableMemoryRenderer(memoryRendererA); DisableMemoryRenderer(memoryRendererB); activeMemoryRenderer = null; StopTruthLieFlicker(); SilenceNoiseOverlayIfPresent(); } /// /// 演员大字闪现:glitch 一拍的瞬间隐藏抖动的粒子字,屏幕中央出现白色大字。 /// 三次冲击逐级增强并累积记忆损伤;普通闪现结束后始终退回抖动粒子字。 /// 只有 expression_actor_flash_in 会保持大字,用于明确的结尾定格。 /// public IEnumerator FlashActorLie( string text, float holdDuration, float fontSize = 10.5f, float settledScaleValue = -1f, float punchScaleValue = -1f, float chromaDistance = -1f, float chromaAlpha = -1f, float impact = -1f, float glitchPeak = -1f, bool keepVisible = false) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!ExpectState(nameof(FlashActorLie), PresentationState.Memory)) yield break; if (actorFlashActive) { Debug.LogWarning("[LogReleasePresentation] expression_actor_flash 在上一次闪现结束前被调用;已安全跳过。"); yield break; } actorFlashActive = true; PulseFaceErrorSprite(Mathf.Max(0.1f, holdDuration)); actorFlashCount = Mathf.Clamp(actorFlashCount + 1, 1, 3); int flashLevel = actorFlashCount; float flashProgress = flashLevel / 3f; float resolvedImpact = impact >= 0f ? Mathf.Clamp01(impact) : GetFlashImpact(flashLevel); float resolvedGlitchPeak = glitchPeak >= 0f ? Mathf.Clamp01(glitchPeak) : Mathf.Lerp(0.16f, 0.42f, flashProgress); float resolvedChromaAlpha = chromaAlpha >= 0f ? Mathf.Clamp01(chromaAlpha) : Mathf.Lerp(0.06f, 0.12f, flashProgress); float resolvedChromaDistance = chromaDistance >= 0f ? chromaDistance : Mathf.Lerp(0.006f, 0.014f, flashProgress); float resolvedSettledScale = settledScaleValue > 0f ? settledScaleValue : Mathf.Lerp(1.15f, 1.25f, flashProgress); float resolvedPunchScale = punchScaleValue > 0f ? punchScaleValue : resolvedSettledScale * Mathf.Lerp(1.28f, 1.42f, flashProgress); memoryDamage = Mathf.Max(memoryDamage, GetFlashDamage(flashLevel)); memoryTearSeed = Mathf.Repeat(memoryTearSeed + 0.271f + flashLevel * 0.137f, 1f); StartCoroutine(PulseMemoryImpact( resolvedImpact, 0.045f, Mathf.Max(0.05f, holdDuration - 0.08f), Mathf.Lerp(0.26f, 0.16f, flashProgress))); try { if (resolvedGlitchPeak > 0.001f) StartCoroutine(PulseGlitch(resolvedGlitchPeak, 0.04f, 0.14f)); particleManager?.SetTargetParticlesAlpha(0f, 0f); PositionScreenText(); string value = text ?? string.Empty; float resolvedFontSize = Mathf.Max(0.1f, fontSize); actorFlashText.fontSize = resolvedFontSize; actorFlashChromaRed.fontSize = resolvedFontSize; actorFlashChromaCyan.fontSize = resolvedFontSize; SetTextActive(actorFlashText, value, Color.white); SetTextActive(actorFlashChromaRed, value, new Color(1f, 0.25f, 0.25f, resolvedChromaAlpha)); SetTextActive(actorFlashChromaCyan, value, new Color(0.3f, 0.9f, 1f, resolvedChromaAlpha)); Vector3 center = actorFlashText.transform.position; actorFlashChromaRed.transform.position = center + Vector3.left * resolvedChromaDistance; actorFlashChromaCyan.transform.position = center + Vector3.right * resolvedChromaDistance; const float punchDuration = 0.14f; Vector3 settledScale = Vector3.one * resolvedSettledScale; Vector3 punchScale = Vector3.one * resolvedPunchScale; actorFlashText.transform.localScale = punchScale; actorFlashChromaRed.transform.localScale = punchScale; actorFlashChromaCyan.transform.localScale = punchScale; actorFlashText.transform.DOScale(settledScale, punchDuration).SetEase(Ease.OutCubic).SetTarget(this); actorFlashChromaRed.transform.DOScale(settledScale, punchDuration).SetEase(Ease.OutCubic).SetTarget(this); actorFlashChromaCyan.transform.DOScale(settledScale, punchDuration).SetEase(Ease.OutCubic).SetTarget(this); yield return new WaitForSeconds(Mathf.Max(0.05f, holdDuration)); } finally { actorFlashActive = false; bool holdForOverdrive = keepVisible; if (!holdForOverdrive) HideActorFlashVisualsImmediate(); if (!holdForOverdrive && state == PresentationState.Memory) particleManager?.SetTargetParticlesAlpha(1f, 0f); } } /// /// 真话闪现:谎话大字之间,暖色完整真话带着细微抖动挣扎浮现一拍。 /// 不推进谎话冲击等级、不累积记忆损伤;"被掐断"由紧随其后的 expression_glitch_pulse 表现。 /// public IEnumerator FlashActorTruth( string text, float holdDuration, float fontSize = 8.5f, float alpha = 0.8f, float jitter = 0.02f) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!ExpectState(nameof(FlashActorTruth), PresentationState.Memory)) yield break; if (truthFlashActive) { Debug.LogWarning("[LogReleasePresentation] expression_actor_truth_flash 在上一次闪现结束前被调用;已安全跳过。"); yield break; } truthFlashActive = true; try { PositionScreenText(); string value = text ?? string.Empty; truthFlashText.fontSize = Mathf.Max(0.1f, fontSize); Color warm = truthAccentColor; warm.a = 0f; SetTextActive(truthFlashText, value, warm); truthFlashText.transform.localScale = Vector3.one; FadeText(truthFlashText, Mathf.Clamp01(alpha), 0.09f); Vector3 origin = truthFlashText.transform.position; float shake = Mathf.Max(0f, jitter); float elapsed = 0f; float hold = Mathf.Max(0.05f, holdDuration); while (elapsed < hold && state == PresentationState.Memory) { truthFlashText.transform.position = origin + new Vector3( UnityEngine.Random.Range(-shake, shake), UnityEngine.Random.Range(-shake, shake) * 0.6f, 0f); elapsed += Time.deltaTime; yield return null; } truthFlashText.transform.position = origin; } finally { truthFlashActive = false; HideTextImmediate(truthFlashText); } } /// /// 老虎机换字演出:聚焦字在 duration 内只从 charPool 里疯狂换字(非阻塞,节拍由 Yarn wait 控制)。 /// truthPool 非空时其字符也混入换字池,并在主字上以真话暖色显示——真假字符交替闪烁。 /// public void StartActorSlotMachine( string charPool, float duration, float interval, float fontSize = 14f, string truthPool = "") { EnsureRuntimeObjects(); if (!ExpectState(nameof(StartActorSlotMachine), PresentationState.Memory)) return; string pool = LanguageParticleManager.SanitizeSlotMachinePool(charPool); if (string.IsNullOrEmpty(pool)) { Debug.LogWarning("[LogReleasePresentation] 演员老虎机字符池为空;已安全跳过。"); return; } string truthCharacters = LanguageParticleManager.SanitizeSlotMachinePool(truthPool) ?? string.Empty; pool += truthCharacters; StopActorOverdriveImmediate(); particleManager?.SetTargetParticlesAlpha(0f, 0f); float resolvedFontSize = Mathf.Max(0.1f, fontSize); actorFlashText.fontSize = resolvedFontSize; actorFlashChromaRed.fontSize = resolvedFontSize; actorFlashChromaCyan.fontSize = resolvedFontSize; actorOverdriveRoutine = StartCoroutine(ActorOverdriveRoutine( pool, Mathf.Max(0.05f, duration), Mathf.Max(0.02f, interval), truthCharacters)); StartCoroutine(PulseGlitch( 0.64f, Mathf.Max(0.05f, duration * 0.82f), Mathf.Max(0.05f, duration * 0.18f))); } public IEnumerator PlayActorWordHit( string text, float holdDuration, float fontSize = 18f, float punchScale = 1.55f) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!ExpectState(nameof(PlayActorWordHit), PresentationState.Memory)) yield break; StopActorOverdriveImmediate(); HideActorScrollImmediate(); HideActorFlashVisualsImmediate(); particleManager?.SetTargetParticlesAlpha(0f, 0f); PulseFaceErrorSprite(Mathf.Max(0.1f, holdDuration)); PositionScreenText(); string value = text ?? string.Empty; actorFlashText.fontSize = Mathf.Max(0.1f, fontSize); SetTextActive(actorFlashText, value, Color.white); actorFlashText.transform.localScale = Vector3.one * Mathf.Max(0.1f, punchScale); actorFlashText.transform.DOScale(Vector3.one, 0.065f) .SetEase(Ease.OutCubic) .SetTarget(actorFlashText); memoryTearSeed = Mathf.Repeat(memoryTearSeed + 0.239f, 1f); memoryImpact = 1f; yield return new WaitForSeconds(Mathf.Max(0.05f, holdDuration)); HideTextImmediate(actorFlashText); actorFlashText.fontSize = 10.5f; particleManager?.SetTargetParticlesAlpha(1f, 0f); yield return new WaitForSeconds(0.025f); } public IEnumerator ScrollActorLies( string phraseList, float scrollDuration, float loopDuration, float holdDuration, float fontSize = 5.2f) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!ExpectState(nameof(ScrollActorLies), PresentationState.Memory)) yield break; string[] phrases = ParseActorPhrases(phraseList); if (phrases.Length == 0) { Debug.LogWarning("[LogReleasePresentation] 向下刷屏词组为空;已安全跳过。"); yield break; } StopActorOverdriveImmediate(); HideActorFlashVisualsImmediate(); HideActorScrollImmediate(); particleManager?.SetTargetParticlesAlpha(0f, 0f); const int itemCount = 4; EnsureActorScrollTextPool(itemCount); Bounds screen = GetScreenBounds(); float rowSpacing = screen.size.y / (itemCount - 1f); float wrapSpan = rowSpacing * itemCount; float speed = wrapSpan / Mathf.Max(0.20f, loopDuration); float[] yPositions = new float[itemCount]; int[] phraseIndices = new int[itemCount]; for (int i = 0; i < actorScrollTexts.Count; i++) { TextMeshPro text = actorScrollTexts[i]; bool used = i < itemCount; text.gameObject.SetActive(used); if (!used) continue; phraseIndices[i] = i % phrases.Length; yPositions[i] = screen.max.y + rowSpacing * (i - 1); text.fontSize = Mathf.Max(0.1f, fontSize); text.rectTransform.sizeDelta = screen.size; SetTextActive(text, phrases[phraseIndices[i]], new Color(1f, 1f, 1f, 0.92f)); text.transform.position = new Vector3(screen.center.x, yPositions[i], screen.center.z); text.transform.localScale = Vector3.one; } float elapsed = 0f; float moveDuration = Mathf.Max(0.05f, scrollDuration); while (elapsed < moveDuration && state == PresentationState.Memory) { float delta = Time.deltaTime; for (int i = 0; i < itemCount; i++) { TextMeshPro text = actorScrollTexts[i]; yPositions[i] -= speed * delta; if (yPositions[i] < screen.min.y - rowSpacing) { yPositions[i] += wrapSpan; phraseIndices[i] = (phraseIndices[i] + 1) % phrases.Length; text.text = phrases[phraseIndices[i]]; } Vector3 position = text.transform.position; position.y = yPositions[i]; text.transform.position = position; } elapsed += delta; yield return null; } if (state == PresentationState.Memory) yield return new WaitForSeconds(Mathf.Max(0f, holdDuration)); HideActorScrollImmediate(); } public void SnapTruthImmediate(float peripheral, float calmTarget) { EnsureRuntimeObjects(); if (!ExpectState( nameof(SnapTruthImmediate), PresentationState.Memory, PresentationState.LieHolding, PresentationState.LieCracking)) return; state = PresentationState.TruthResolving; StopLieShuffleImmediate(true); HideLieLayersImmediate(); StopScanline(); StopScreenDim(); particleManager?.PrepareTruthReleaseFromLie(); particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor); particleManager?.SetCurrentPresentationPhraseAlpha(1f, 0f); Bounds truthBounds = particleManager != null ? particleManager.GetFinalTruthWorldBounds() : new Bounds(GetScreenBounds().center, Vector3.one); SetCalmField(truthBounds); effectStrength = Mathf.Clamp01(peripheral); calmRadius = Mathf.Clamp01(calmTarget); particleManager?.SnapTruthImmediate(); state = PresentationState.TruthHolding; } /// 真话聚合尝试(非等待):目标字被慢慢拉向最终位置并按序逐字锁定,感觉即将成功。 public void BeginTruthGatherAttempt(float target, float duration) { if (!ExpectState(nameof(BeginTruthGatherAttempt), PresentationState.Memory)) return; particleManager?.BeginTruthGather(target, duration); } /// 真话聚合被打断:目标字向外爆散并恢复乱跳(非等待)。 public void ScatterTruthGatherAttempt(float burstDistance) { if (!ExpectState(nameof(ScatterTruthGatherAttempt), PresentationState.Memory)) return; particleManager?.ScatterTruthGather(burstDistance); } /// 真话聚合挣扎(非等待):推进途中反复"够到又被拽回去",字会锁上再崩开。 public void BeginTruthStrainAttempt(float target, float duration, int tugs) { if (!ExpectState(nameof(BeginTruthStrainAttempt), PresentationState.Memory)) return; particleManager?.BeginTruthStrain(target, duration, tugs); } /// 真话聚合崩塌(非等待):进度被拽回 0,所有字沿原路退回并恢复乱跳。 public void CollapseTruthGatherAttempt(float duration) { if (!ExpectState(nameof(CollapseTruthGatherAttempt), PresentationState.Memory)) return; particleManager?.CollapseTruthGather(duration); } /// 火山脸错误闪红:聚合失败的报错信号(非等待)。 public void PlayFaceErrorFlash(float duration, float intensity) { if (faceRenderer == null) return; _ = intensity; // 兼容旧 Yarn 参数;红屏 Sprite 本身决定最终颜色。 float total = Mathf.Max(0.1f, duration); PulseFaceErrorSprite(total); DipFace(total * 0.8f, faceDipRatio * 1.6f); } private void PulseFaceErrorSprite(float duration) { if (faceRenderer == null || errorFaceSprite == null) return; if (faceErrorRoutine != null) StopCoroutine(faceErrorRoutine); if (faceRenderer.sprite != errorFaceSprite) normalFaceSprite = faceRenderer.sprite; faceRenderer.sprite = errorFaceSprite; faceErrorRoutine = StartCoroutine(RestoreFaceSpriteAfter(Mathf.Max(0.05f, duration))); } private IEnumerator RestoreFaceSpriteAfter(float duration) { yield return new WaitForSeconds(duration); RestoreFaceSpriteImmediate(); } private void RestoreFaceSpriteImmediate() { if (faceRenderer != null && normalFaceSprite != null) faceRenderer.sprite = normalFaceSprite; faceErrorRoutine = null; } /// /// 结尾真假快速闪切(非等待):真心话与谎话交替独占屏幕,两者的屏幕底色(电视头背景)不同, /// 越到后面切得越快,最后定格在真话帧。节奏由同长度的 Yarn wait 控制。 /// public void StartTruthLieFlicker( string liePhrases, string truthPhrase, float duration, float interval, float fontSize) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!ExpectState(nameof(StartTruthLieFlicker), PresentationState.Memory)) return; string[] lies = ParseActorPhrases(liePhrases); if (lies.Length == 0 || string.IsNullOrEmpty(truthPhrase)) { Debug.LogWarning("[LogReleasePresentation] 真假闪切词组为空;已安全跳过。"); return; } StopTruthLieFlicker(); StopActorOverdriveImmediate(); particleManager?.SetTargetParticlesAlpha(0f, 0f); truthLieFlickerRoutine = StartCoroutine(TruthLieFlickerRoutine( lies, truthPhrase, Mathf.Max(0.2f, duration), Mathf.Max(0.05f, interval), Mathf.Max(0.1f, fontSize))); } private IEnumerator TruthLieFlickerRoutine( string[] lies, string truthPhrase, float duration, float interval, float fontSize) { PositionScreenText(); actorFlashText.fontSize = fontSize; actorFlashText.transform.localScale = Vector3.one; HideTextImmediate(actorFlashChromaRed); HideTextImmediate(actorFlashChromaCyan); ShowFlickerBackdrop(); bool truthTurn = false; int lieIndex = 0; float elapsed = 0f; while (elapsed < duration && state == PresentationState.Memory) { ShowFlickerFrame(truthTurn, lies, lieIndex, truthPhrase); if (!truthTurn) lieIndex++; truthTurn = !truthTurn; // 越到后面切得越快:真话越来越压不住 float progress = Mathf.Clamp01(elapsed / duration); float step = Mathf.Lerp(interval, interval * 0.45f, progress); step = Mathf.Min(step, duration - elapsed); elapsed += step; yield return new WaitForSeconds(Mathf.Max(0.02f, step)); } // 最后停在第一条谎话("没问题"),交给 expression_lie_morph_truth 做 glitch 翻转 if (state == PresentationState.Memory) ShowFlickerFrame(false, lies, 0, truthPhrase); truthLieFlickerRoutine = null; } private void ShowFlickerFrame(bool truth, string[] lies, int lieIndex, string truthPhrase) { if (truth) { Color warm = truthAccentColor; warm.a = 1f; SetTextActive(actorFlashText, truthPhrase, warm); SetFlickerBackdropColor(truthBackdropColor); } else { SetTextActive(actorFlashText, lies[lieIndex % lies.Length], Color.white); SetFlickerBackdropColor(lieBackdropColor); } } private void ShowFlickerBackdrop() { if (flickerBackdropRenderer == null) return; Bounds screen = GetScreenBounds(); flickerBackdropRenderer.transform.position = screen.center; flickerBackdropRenderer.transform.localScale = new Vector3(screen.size.x, screen.size.y, 1f); flickerBackdropRenderer.enabled = true; flickerBackdropRenderer.gameObject.SetActive(true); } private void SetFlickerBackdropColor(Color color) { if (flickerBackdropRenderer != null) flickerBackdropRenderer.color = color; } private void HideFlickerBackdropImmediate() { if (flickerBackdropRenderer == null) return; flickerBackdropRenderer.enabled = false; flickerBackdropRenderer.gameObject.SetActive(false); } private void StopTruthLieFlicker() { if (truthLieFlickerRoutine != null) { StopCoroutine(truthLieFlickerRoutine); truthLieFlickerRoutine = null; } HideFlickerBackdropImmediate(); } /// /// 定格的谎话被 glitch 翻转成真话(等待):大字在乱跳中长度渐变、真假底色抢闪, /// 最后落在暖色真话并轻微收缩定住。后续由 expression_memory_cut + expression_truth_snap 收尾。 /// public IEnumerator MorphLieToTruth(string lieValue, string truthValue, float duration) { EnsureRuntimeObjects(); EnsureScreenTextClipMaterials(); if (!ExpectState(nameof(MorphLieToTruth), PresentationState.Memory)) yield break; string lie = lieValue ?? string.Empty; string truth = truthValue ?? string.Empty; string pool = lie + truth; if (pool.Length == 0 || truth.Length == 0) { Debug.LogWarning("[LogReleasePresentation] expression_lie_morph_truth 文本为空;已安全跳过。"); yield break; } if (truthLieFlickerRoutine != null) { StopCoroutine(truthLieFlickerRoutine); truthLieFlickerRoutine = null; } StopActorOverdriveImmediate(); particleManager?.SetTargetParticlesAlpha(0f, 0f); PositionScreenText(); ShowFlickerBackdrop(); HideTextImmediate(actorFlashChromaRed); HideTextImmediate(actorFlashChromaCyan); Vector3 origin = actorFlashText.transform.position; SetTextActive(actorFlashText, lie, Color.white); SetFlickerBackdropColor(lieBackdropColor); float total = Mathf.Max(0.2f, duration); float scrambleDuration = total * 0.7f; float elapsed = 0f; bool warmTick = false; while (elapsed < scrambleDuration && state == PresentationState.Memory) { float progress = Mathf.Clamp01(elapsed / scrambleDuration); int length = Mathf.Clamp( Mathf.RoundToInt(Mathf.Lerp(Mathf.Max(1, lie.Length), truth.Length, progress)), 1, Mathf.Max(1, truth.Length)); warmTick = !warmTick; Color color = warmTick ? truthAccentColor : Color.white; color.a = 1f; SetTextActive(actorFlashText, BuildRandomActorText(pool, length), color); SetFlickerBackdropColor(warmTick ? truthBackdropColor : lieBackdropColor); actorFlashText.transform.position = origin + (Vector3)(UnityEngine.Random.insideUnitCircle * Mathf.Lerp(0.01f, 0.05f, progress)); float step = Mathf.Lerp(0.09f, 0.035f, progress); step = Mathf.Min(step, scrambleDuration - elapsed); elapsed += step; yield return new WaitForSeconds(Mathf.Max(0.02f, step)); } if (state != PresentationState.Memory) yield break; actorFlashText.transform.position = origin; Color warm = truthAccentColor; warm.a = 1f; SetTextActive(actorFlashText, truth, warm); SetFlickerBackdropColor(truthBackdropColor); actorFlashText.transform.localScale = Vector3.one * 1.12f; actorFlashText.transform.DOScale(Vector3.one, 0.12f) .SetEase(Ease.OutCubic) .SetTarget(this); yield return new WaitForSeconds(Mathf.Max(0.05f, total - scrambleDuration)); } /// /// 记忆材质上的瞬时失真脉冲(抬升 _SimGlitch 后回落),不再驱动 noise 叠层。 /// 撕裂仍走 PlayMemoryTear / impact,避免两路抢同一参数。 /// public void PlayGlitchPulse(float peak, float riseDuration, float fallDuration) { if (!ExpectState( nameof(PlayGlitchPulse), PresentationState.Memory, PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthResolving, PresentationState.TruthHolding)) return; StartCoroutine(PulseGlitch( Mathf.Clamp01(peak), Mathf.Max(0.01f, riseDuration), Mathf.Max(0.01f, fallDuration))); } /// /// 已弃用:屏幕 Glitch 请走 ExpressionManager.SetScreenGlitchLevel(屏幕自身材质)。 /// 记忆阶段失真请用 PlayGlitchPulse / 记忆 impact。 /// public void PlayScreenGlitch(float intensity, float duration) { _ = intensity; _ = duration; } public IEnumerator PlayTruthAttack( string text, float duration, float impact) { if (!ExpectState( nameof(PlayTruthAttack), PresentationState.Memory, PresentationState.TruthResolving, PresentationState.TruthHolding)) yield break; float resolvedImpact = Mathf.Clamp01(impact); // Truth attacks inherit the memory-layer damage language used by actor hits: // each hit advances a deterministic tear layout and drives a short impact pulse. memoryDamage = Mathf.Max( memoryDamage, Mathf.Lerp(0.18f, 0.88f, resolvedImpact)); memoryTearSeed = Mathf.Repeat( memoryTearSeed + 0.193f + resolvedImpact * 0.211f, 1f); StartCoroutine(PulseMemoryImpact( Mathf.Lerp(0.24f, 1f, resolvedImpact), Mathf.Lerp(0.065f, 0.025f, resolvedImpact), Mathf.Lerp(0.035f, 0.12f, resolvedImpact), Mathf.Lerp(0.22f, 0.12f, resolvedImpact))); PlayMemoryJolt( Mathf.Lerp(0.018f, 0.075f, resolvedImpact), Mathf.Lerp(0.12f, 0.28f, resolvedImpact), Mathf.Lerp(0.025f, 0.16f, resolvedImpact)); PlayGlitchPulse( Mathf.Lerp(0.10f, 0.58f, resolvedImpact), 0.035f, Mathf.Lerp(0.10f, 0.22f, resolvedImpact)); PlayMemoryTear( Mathf.Lerp(0.08f, 0.58f, resolvedImpact), Mathf.Lerp(0.14f, 0.36f, resolvedImpact)); PulseTruthAttackFlash( Mathf.Lerp( truthAttackFlashAlpha.x, truthAttackFlashAlpha.y, resolvedImpact), Mathf.Lerp(0.035f, 0.018f, resolvedImpact), Mathf.Lerp(0.18f, 0.30f, resolvedImpact)); if (particleManager != null) yield return particleManager.PlayTruthAttackAndWait(text, duration, resolvedImpact); } public IEnumerator DestabilizeTruthPhrase( string text, float duration, float shakeAmplitude, float shakeSpeed, string colorHex) { if (!ExpectState( nameof(DestabilizeTruthPhrase), PresentationState.Memory, PresentationState.TruthResolving, PresentationState.TruthHolding)) yield break; if (particleManager != null) { yield return particleManager.DestabilizeTruthPhraseAndWait( text, duration, shakeAmplitude, shakeSpeed, colorHex); } } public IEnumerator SettleTruthPhrase(string text, float duration) { if (!ExpectState( nameof(SettleTruthPhrase), PresentationState.Memory, PresentationState.TruthResolving, PresentationState.TruthHolding)) yield break; float settleDuration = Mathf.Max(0.08f, duration * 0.65f); DOTween.To( () => simGlitch, value => simGlitch = value, memoryBaseSimGlitch, settleDuration) .SetEase(Ease.OutQuad) .SetTarget(this); DOTween.To( () => horizontalTear, value => horizontalTear = value, 0f, settleDuration) .SetEase(Ease.OutQuad) .SetTarget(this); if (particleManager != null) yield return particleManager.SettleTruthPhraseAndWait(text, duration); } public IEnumerator BuildActorOverloadPeak( float riseDuration, float holdDuration, float zoom, float glitchPeak, float impact) { EnsureRuntimeObjects(); if (!ExpectState(nameof(BuildActorOverloadPeak), PresentationState.Memory)) yield break; float rise = Mathf.Max(0.01f, riseDuration); float resolvedZoom = Mathf.Max(1f, zoom); float resolvedImpact = Mathf.Clamp01(impact); memoryTearSeed = Mathf.Repeat(memoryTearSeed + 0.347f, 1f); DOTween.To(() => memoryZoom, value => memoryZoom = value, resolvedZoom, rise) .SetEase(Ease.InQuad) .SetTarget(this); DOTween.To(() => memoryImpact, value => memoryImpact = value, resolvedImpact, rise) .SetEase(Ease.InQuad) .SetTarget(this); DOTween.To(() => memoryOverdrive, value => memoryOverdrive = value, 1f, rise) .SetEase(Ease.InQuad) .SetTarget(this); float peak = Mathf.Clamp01(glitchPeak); DOTween.To( () => simGlitch, value => simGlitch = value, Mathf.Lerp(memoryBaseSimGlitch, 1f, peak), rise) .SetEase(Ease.InQuad) .SetTarget(this); DOTween.To( () => horizontalTear, value => horizontalTear = value, Mathf.Lerp(0.18f, 0.72f, peak), rise) .SetEase(Ease.InQuad) .SetTarget(this); if (actorFlashText != null) { actorFlashText.transform.DOScale( actorFlashText.transform.localScale * resolvedZoom, rise) .SetEase(Ease.InQuad) .SetTarget(this); } if (actorFlashChromaRed != null) { actorFlashChromaRed.transform.DOScale( actorFlashChromaRed.transform.localScale * resolvedZoom, rise) .SetEase(Ease.InQuad) .SetTarget(this); } if (actorFlashChromaCyan != null) { actorFlashChromaCyan.transform.DOScale( actorFlashChromaCyan.transform.localScale * resolvedZoom, rise) .SetEase(Ease.InQuad) .SetTarget(this); } yield return new WaitForSeconds(rise); yield return new WaitForSeconds(Mathf.Max(0f, holdDuration)); } public IEnumerator HoldTruthBlackout(float duration, float alpha) { if (!ExpectState( nameof(HoldTruthBlackout), PresentationState.Memory, PresentationState.LieHolding, PresentationState.LieCracking)) yield break; // 顿挫一拍:黑场盖住真假闪切的底色与大字 StopTruthLieFlicker(); HideTextImmediate(actorFlashText); HoldTruthBreakBlackout(alpha); yield return new WaitForSeconds(Mathf.Max(0.01f, duration)); } public void SplitLieVisual(float duration, float distanceRatio, float stretchX, float squashY) { EnsureRuntimeObjects(); if (!ExpectState( nameof(SplitLieVisual), PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthResolving)) return; if (lieText == null || !lieText.gameObject.activeSelf) return; Bounds screenBounds = GetScreenBounds(); Vector3 lieOrigin = lieText.transform.position; float tweenDuration = Mathf.Max(0.01f, duration); float distance = screenBounds.extents.x * Mathf.Max(0f, distanceRatio); SetTextActive(lieGhostText, currentLieKeyword, truthWarmColor); lieGhostText.transform.position = lieOrigin + Vector3.left * 0.10f; lieGhostText.transform.localScale = Vector3.one; lieGhostText.characterSpacing = lieText.characterSpacing; SetTextAlpha(lieGhostText, 0.90f); lieText.transform.DOScale( new Vector3(Mathf.Max(0.01f, stretchX), Mathf.Max(0.01f, squashY), 1f), tweenDuration) .SetEase(Ease.InCubic) .SetTarget(this); lieText.transform.DOMoveX(lieOrigin.x + distance, tweenDuration) .SetEase(Ease.InCubic) .SetTarget(this); lieGhostText.transform.DOMoveX(lieOrigin.x - distance, tweenDuration) .SetEase(Ease.InCubic) .SetTarget(this); FadeText(lieText, 0f, tweenDuration); FadeText(lieGhostText, 0f, tweenDuration); } public void PlayTruthFlash(float alpha, float riseDuration, float holdDuration, float fallDuration) { EnsureRuntimeObjects(); if (!ExpectState( nameof(PlayTruthFlash), PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthResolving, PresentationState.TruthHolding)) return; PulseTruthBreakFlash(alpha, riseDuration, holdDuration, fallDuration); } public void PlayMemoryBrightnessPulse(float amount, float riseDuration, float fallDuration) { if (!ExpectState( nameof(PlayMemoryBrightnessPulse), PresentationState.Memory, PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthResolving, PresentationState.TruthHolding)) return; PulseMemoryRelease(amount, riseDuration, fallDuration); } public void PlayMemoryTear(float peak, float duration) { if (!ExpectState( nameof(PlayMemoryTear), PresentationState.Memory, PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthResolving, PresentationState.TruthHolding)) return; horizontalTear = Mathf.Max(horizontalTear, Mathf.Clamp01(peak)); DOTween.To( () => horizontalTear, value => horizontalTear = value, 0f, Mathf.Max(0.01f, duration)) .SetEase(Ease.OutQuad) .SetTarget(this); } public void PlayFaceDip(float duration, float ratio) { if (!ExpectState( nameof(PlayFaceDip), PresentationState.LieHolding, PresentationState.LieCracking, PresentationState.TruthResolving)) return; DipFace(Mathf.Max(0.01f, duration), Mathf.Max(0f, ratio)); } public void ReleaseTruthCharacters(float alphaDuration) { if (!ExpectState( nameof(ReleaseTruthCharacters), PresentationState.LieHolding, PresentationState.LieCracking)) return; state = PresentationState.TruthResolving; particleManager?.PrepareTruthReleaseFromLie(); particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor); particleManager?.SetTargetParticlesAlpha(1f, Mathf.Max(0.01f, alphaDuration)); Bounds truthBounds = particleManager != null ? particleManager.GetFinalTruthWorldBounds() : new Bounds(GetScreenBounds().center, Vector3.one); SetCalmField(truthBounds); } public IEnumerator ResolveTruthCharacters(float duration, float peripheral, float calmTarget) { if (!ExpectState(nameof(ResolveTruthCharacters), PresentationState.TruthResolving)) yield break; float resolveDuration = Mathf.Max(0.01f, duration); DOTween.To( () => effectStrength, value => effectStrength = value, Mathf.Clamp01(peripheral), resolveDuration) .SetEase(Ease.OutQuad) .SetTarget(this); calmRadius = 0f; DOTween.To( () => calmRadius, value => calmRadius = value, Mathf.Clamp01(calmTarget), resolveDuration) .SetEase(Ease.OutCubic) .SetTarget(this); brightness = Mathf.Max(brightness, currentMemoryKind == MemoryKind.Sunset ? 1.02f : brightness); if (particleManager != null) yield return particleManager.ResolveFocusAndWait(resolveDuration); else yield return new WaitForSeconds(resolveDuration); if (currentMemoryKind == MemoryKind.Sunset) { DOTween.To(() => saturation, value => saturation = value, 0.92f, 0.2f) .SetEase(Ease.OutQuad) .SetTarget(this); } HideLieLayersImmediate(); StopScanline(); StopScreenDim(); ReleaseScreenTextClipMaterials(); DOTween.To(() => simGlitch, value => simGlitch = value, memoryBaseSimGlitch, 0.18f) .SetEase(Ease.OutQuad) .SetTarget(this); DOTween.To(() => horizontalTear, value => horizontalTear = value, 0f, 0.18f) .SetEase(Ease.OutQuad) .SetTarget(this); state = PresentationState.TruthHolding; } public void TruthImpact() { EnsureRuntimeObjects(); if (!ExpectState(nameof(TruthImpact), PresentationState.TruthHolding, PresentationState.TruthResolving)) return; StartCoroutine(TruthImpactRoutine()); } public IEnumerator EndMemory() { EnsureRuntimeObjects(); if (!ExpectState( nameof(EndMemory), PresentationState.Memory, PresentationState.LieCracking, PresentationState.TruthHolding)) { yield break; } state = PresentationState.Returning; TweenMemoryOpacity(memoryRendererA, 0f, memoryExitDuration); TweenMemoryOpacity(memoryRendererB, 0f, memoryExitDuration); HideLieLayersImmediate(); StopScanline(); StopScreenDim(); SilenceNoiseOverlayIfPresent(); DOTween.To(() => simGlitch, value => simGlitch = value, 0f, 0.12f) .SetEase(Ease.OutQuad) .SetTarget(this); DOTween.To(() => horizontalTear, value => horizontalTear = value, 0f, 0.12f) .SetEase(Ease.OutQuad) .SetTarget(this); yield return new WaitForSeconds(memoryExitDuration); DisableMemoryRenderer(memoryRendererA); DisableMemoryRenderer(memoryRendererB); if (currentMemoryKind == MemoryKind.PrivateOffice) particleManager?.SetTargetParticlesAlpha(0.6f, 0.18f); state = PresentationState.TruthHolding; } public void ForceCleanupImmediate(bool hideFace) { DOTween.Kill(this); StopAllCoroutines(); RestoreFaceSpriteImmediate(); memoryOpacityA = 0f; memoryOpacityB = 0f; DisableMemoryRenderer(memoryRendererA); DisableMemoryRenderer(memoryRendererB); activeMemoryRenderer = null; HideLieLayersImmediate(); StopScanline(); StopScreenDim(); ReleaseScreenTextClipMaterials(); if (faceRenderer != null) { faceRenderer.transform.position = faceBasePosition; if (hideFace) { Color faceColor = faceRenderer.color; faceColor.a = 0f; faceRenderer.color = faceColor; faceRenderer.enabled = false; faceRenderer.gameObject.SetActive(false); } } ResetNoiseOverlayForGlitchTransition(); particleManager?.RestoreTargetPresentationDefaults(); state = PresentationState.Idle; currentLieKeyword = string.Empty; memoryPushSpeed = 0f; memoryPushProgress = 0f; memoryZoom = 1f; horizontalTear = 0f; noise = 0f; simGlitch = 0f; memoryBaseSimGlitch = 0f; memoryImpact = 0f; memoryDamage = 0f; memoryOverdrive = 0f; calmRadius = 0f; } private static float GetFlashDamage(int flashLevel) { return flashLevel switch { 1 => 0.18f, 2 => 0.40f, _ => 0.66f }; } private static float GetFlashImpact(int flashLevel) { return flashLevel switch { 1 => 0.46f, 2 => 0.72f, _ => 1f }; } private IEnumerator PulseMemoryImpact( float peak, float riseDuration, float holdDuration, float fallDuration) { float start = memoryImpact; yield return DOTween.To( () => memoryImpact, value => memoryImpact = value, Mathf.Clamp01(peak), Mathf.Max(0.01f, riseDuration)) .SetEase(Ease.OutQuad) .SetTarget(this) .WaitForCompletion(); yield return new WaitForSeconds(Mathf.Max(0f, holdDuration)); yield return DOTween.To( () => memoryImpact, value => memoryImpact = value, start, Mathf.Max(0.01f, fallDuration)) .SetEase(Ease.OutCubic) .SetTarget(this) .WaitForCompletion(); } private IEnumerator ActorOverdriveRoutine(string pool, float duration, float interval, string truthPool = "") { PositionScreenText(); string initial = actorFlashText != null ? actorFlashText.text : string.Empty; int characterCount = Mathf.Max(1, string.IsNullOrEmpty(initial) ? 3 : initial.Length); float elapsed = 0f; float nextShuffle = 0f; while (elapsed < duration && state == PresentationState.Memory) { float progress = Mathf.Clamp01(elapsed / duration); float eased = progress * progress * (3f - 2f * progress); // 起点压低,让老虎机段有"从可辨认到失控"的爬升;峰值仍到 1 memoryOverdrive = Mathf.Lerp(0.15f, 1f, eased); memoryDamage = Mathf.Max(memoryDamage, Mathf.Lerp(0.45f, 1f, eased)); if (elapsed >= nextShuffle) { string value = BuildRandomActorText(pool, characterCount); SetActorOverdriveText(value, eased, truthPool); memoryTearSeed = Mathf.Repeat(memoryTearSeed + 0.173f, 1f); float acceleratingInterval = Mathf.Lerp( interval, Mathf.Max(0.02f, interval * 0.42f), eased); nextShuffle = elapsed + acceleratingInterval; } float pulse = 1f + Mathf.Sin(elapsed * Mathf.Lerp(12f, 29f, eased)) * Mathf.Lerp(0.025f, 0.095f, eased); Vector3 scale = new Vector3( pulse * Mathf.Lerp(1f, 1.16f, eased), pulse * Mathf.Lerp(1f, 0.90f, eased), 1f); SetActorOverdriveScale(scale); elapsed += Time.deltaTime; yield return null; } if (state == PresentationState.Memory) { memoryImpact = 1f; memoryDamage = 1f; memoryOverdrive = 1f; SetActorOverdriveScale(new Vector3(1.18f, 0.88f, 1f)); } actorOverdriveRoutine = null; } public static string ColorizeTruthCharacters(string value, string truthPool, string colorHex) { if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(truthPool)) return value; var builder = new StringBuilder(value.Length * 8); foreach (char character in value) { if (truthPool.IndexOf(character) >= 0) builder.Append("').Append(character).Append(""); else builder.Append(character); } return builder.ToString(); } private static string BuildRandomActorText(string pool, int characterCount) { if (string.IsNullOrEmpty(pool)) return string.Empty; char[] value = new char[Mathf.Max(1, characterCount)]; for (int i = 0; i < value.Length; i++) value[i] = pool[UnityEngine.Random.Range(0, pool.Length)]; return new string(value); } private void SetActorOverdriveText(string value, float progress, string truthPool = "") { // 主字用富文本按字符归属着色:真话字符=暖色,谎话字符=白。ghost 层保持整体色。 truthWarmColorHex ??= ColorUtility.ToHtmlStringRGB(truthAccentColor); string mainValue = ColorizeTruthCharacters(value, truthPool, truthWarmColorHex); SetTextActive(actorFlashText, mainValue, Color.white); SetTextActive( actorFlashChromaRed, value, new Color(1f, 0.28f, 0.28f, Mathf.Lerp(0.05f, 0.14f, progress))); SetTextActive( actorFlashChromaCyan, value, new Color(0.25f, 0.9f, 1f, Mathf.Lerp(0.05f, 0.14f, progress))); Vector3 center = actorFlashText.transform.position; // 过载段保留轻色散即可,避免主体字被红青双影糊掉 float chromaDistance = Mathf.Lerp(0.008f, 0.028f, progress); float verticalJitter = UnityEngine.Random.Range(-0.004f, 0.004f) * progress; actorFlashChromaRed.transform.position = center + Vector3.left * chromaDistance + Vector3.up * verticalJitter; actorFlashChromaCyan.transform.position = center + Vector3.right * chromaDistance - Vector3.up * verticalJitter; } private void SetActorOverdriveScale(Vector3 scale) { if (actorFlashText != null) actorFlashText.transform.localScale = scale; if (actorFlashChromaRed != null) actorFlashChromaRed.transform.localScale = scale; if (actorFlashChromaCyan != null) actorFlashChromaCyan.transform.localScale = scale; } private void StopActorOverdriveImmediate() { if (actorOverdriveRoutine == null) return; StopCoroutine(actorOverdriveRoutine); actorOverdriveRoutine = null; } public static string[] ParseActorPhrases(string phraseList) { if (string.IsNullOrWhiteSpace(phraseList)) return Array.Empty(); string[] raw = phraseList.Split('|'); var result = new List(raw.Length); foreach (string item in raw) { string value = item.Trim(); if (!string.IsNullOrEmpty(value)) result.Add(value); } return result.ToArray(); } private void EnsureActorScrollTextPool(int count) { while (actorScrollTexts.Count < count) { int index = actorScrollTexts.Count; TextMeshPro text = CreateScreenText( $"ActorScrollText_{index:00}", 3.2f, screenTextSortingOrder - index % 2); TMPRectClipper clipper = text.gameObject.AddComponent(); clipper.Initialize(text, expressionScreenMaskRect); actorScrollTexts.Add(text); actorScrollClippers.Add(clipper); } } private void HideActorScrollImmediate() { foreach (TextMeshPro text in actorScrollTexts) HideTextImmediate(text); } private IEnumerator TruthImpactRoutine() { float original = brightness; float peak = original + 0.18f; yield return DOTween.To(() => brightness, value => brightness = value, peak, 0.08f) .SetEase(Ease.OutQuad) .SetTarget(this) .WaitForCompletion(); yield return DOTween.To(() => brightness, value => brightness = value, original, 0.18f) .SetEase(Ease.InQuad) .SetTarget(this) .WaitForCompletion(); } /// 记忆材质瞬时失真:抬升 _SimGlitch 后回到基线。 private IEnumerator PulseGlitch(float peak, float riseDuration, float fallDuration) { float clampedPeak = Mathf.Clamp01(peak); if (clampedPeak <= 0.001f) yield break; float rise = Mathf.Max(0.01f, riseDuration); float fall = Mathf.Max(0.01f, fallDuration); float startSim = simGlitch; float restoreSim = memoryBaseSimGlitch > 0.001f ? memoryBaseSimGlitch : startSim; float targetSim = Mathf.Max(startSim, Mathf.Lerp(restoreSim, 1f, clampedPeak)); yield return DOTween.To(() => simGlitch, value => simGlitch = value, targetSim, rise) .SetEase(Ease.OutQuad) .SetTarget(this) .WaitForCompletion(); yield return DOTween.To(() => simGlitch, value => simGlitch = value, restoreSim, fall) .SetEase(Ease.InQuad) .SetTarget(this) .WaitForCompletion(); } private void SilenceNoiseOverlayIfPresent() { if (glitchController == null) return; // OpaqueBackground 即使 Intensity=0 仍会写满不透明黑底。 // 记忆阶段切到 TransparentOverlay,避免盖住画面。 glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay); glitchController.Intensity = 0f; glitchController.SetAmbientNoise(0f, 320f, 0f); glitchController.ClearCalmField(true); } /// /// 进记忆交接:把进场前的 noise 从「不透明黑底」切成透明叠层并淡出, /// 让记忆材质自身的 _SimGlitch / Analog 接上失真,而不是突然掐断或继续盖住画面。 /// private void ReleaseNoiseOverlayIntoMemory(float memoryFadeDuration) { if (glitchController == null) return; glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay); glitchController.SetAmbientNoise(0f, 320f, 0f); glitchController.ClearCalmField(true); float releaseDuration = Mathf.Clamp(memoryFadeDuration * 0.35f, 0.12f, 0.55f); if (glitchController.Intensity <= 0.001f) { glitchController.Intensity = 0f; return; } glitchController.TransitionTo(0f, releaseDuration); } private void ResetNoiseOverlayForGlitchTransition() { if (glitchController == null) return; SilenceNoiseOverlayIfPresent(); // 回到 glitch_transition 原始合成;Layer 保持 Default,勿改 HuoshanScreen。 glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.OpaqueBackground); glitchController.gameObject.layer = 0; } private IEnumerator ShuffleLieCharactersRoutine( string characterPool, float duration, float interval) { if (lieText == null || string.IsNullOrEmpty(currentLieKeyword)) yield break; string original = currentLieKeyword; string pool = string.IsNullOrWhiteSpace(characterPool) ? original : characterPool.Replace(" ", string.Empty); if (string.IsNullOrEmpty(pool)) pool = original; float elapsed = 0f; while (elapsed < duration) { float step = Mathf.Min(interval, duration - elapsed); elapsed += step; char[] characters = original.ToCharArray(); int replacementCount = Mathf.Clamp( Mathf.CeilToInt(characters.Length * 0.66f), 1, characters.Length); int startIndex = UnityEngine.Random.Range(0, characters.Length); for (int i = 0; i < replacementCount; i++) { int index = (startIndex + i) % characters.Length; characters[index] = pool[UnityEngine.Random.Range(0, pool.Length)]; } lieText.text = new string(characters); yield return new WaitForSeconds(step); } lieText.text = original; SetTextAlpha(lieText, 1f); lieShuffleRoutine = null; } private void StopLieShuffleImmediate(bool restoreKeyword) { if (lieShuffleRoutine != null) { StopCoroutine(lieShuffleRoutine); lieShuffleRoutine = null; } if (restoreKeyword && lieText != null && !string.IsNullOrEmpty(currentLieKeyword)) lieText.text = currentLieKeyword; } private void DipFace(float duration, float ratio) { if (faceRenderer == null) return; faceRenderer.transform.DOKill(); float height = faceRenderer.bounds.size.y; faceRenderer.transform.DOMoveY(faceBasePosition.y - height * ratio, Mathf.Max(0.06f, duration * 0.35f)) .SetLoops(2, LoopType.Yoyo) .SetEase(Ease.InOutSine) .SetTarget(this) .OnComplete(() => faceRenderer.transform.position = faceBasePosition); } private void SetCalmField(Bounds truthBounds) { if (activeMemoryRenderer != null) { Bounds memoryBounds = activeMemoryRenderer.bounds; if (memoryBounds.size.x > 0.001f && memoryBounds.size.y > 0.001f) { calmCenter = new Vector2( Mathf.InverseLerp(memoryBounds.min.x, memoryBounds.max.x, truthBounds.center.x), Mathf.InverseLerp(memoryBounds.min.y, memoryBounds.max.y, truthBounds.center.y)); } } calmRadius = 0f; calmFeather = 0.20f; } private void ConfigureMemoryLook(MemoryKind kind, Preset preset) { saturation = kind switch { MemoryKind.Office => 0.82f, MemoryKind.Sunset => 0.96f, _ => 0.72f }; brightness = kind switch { MemoryKind.Office => 0.94f, MemoryKind.Sunset => 0.94f, _ => 0.76f }; contrast = kind == MemoryKind.PrivateOffice ? 1.20f : 1.08f; vignette = kind switch { MemoryKind.Office => 0.18f, MemoryKind.Sunset => 0.18f, _ => 0.46f }; // 颗粒噪点与 noise 叠层均弃用;失真只走记忆材质 _SimGlitch。 noise = 0f; simGlitch = preset.Name switch { "Medium" => simGlitchMedium, "Heavy" => simGlitchHeavy, _ => simGlitchLight }; memoryBaseSimGlitch = simGlitch; horizontalTear = 0f; memoryTint = kind switch { MemoryKind.Office => new Color(0.83f, 0.92f, 1f, 1f), MemoryKind.Sunset => new Color(1f, 0.90f, 0.78f, 1f), _ => new Color(0.78f, 0.86f, 1f, 1f) }; calmCenter = new Vector2(0.5f, 0.5f); calmRadius = 0f; calmFeather = 0.18f; effectStrength = preset.BaseDistortion; ApplyMemoryBlocks(); } private void EnsureRuntimeObjects() { if (runtimeObjectsReady) return; if (particleManager == null) particleManager = GetComponentInChildren(true); GameObject rootObject = new GameObject("LogReleasePresentation"); runtimeRoot = rootObject.transform; runtimeRoot.SetParent(transform, false); runtimeWhiteTexture = new Texture2D(1, 1, TextureFormat.RGBA32, false) { name = "Huoshan Presentation White", filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp, hideFlags = HideFlags.DontSave }; runtimeWhiteTexture.SetPixel(0, 0, Color.white); runtimeWhiteTexture.Apply(); runtimeWhiteSprite = Sprite.Create( runtimeWhiteTexture, new Rect(0f, 0f, 1f, 1f), new Vector2(0.5f, 0.5f), 1f); runtimeWhiteSprite.name = "Huoshan Presentation White"; runtimeWhiteSprite.hideFlags = HideFlags.DontSave; memoryRendererA = CreateMemoryRenderer("MemoryRendererA"); memoryRendererB = CreateMemoryRenderer("MemoryRendererB"); memoryBlockA = new MaterialPropertyBlock(); memoryBlockB = new MaterialPropertyBlock(); CreateScreenMask(); lieText = CreateScreenText("LieKeyword", 4.1f, screenTextSortingOrder); lieGhostText = CreateScreenText("LieKeywordGhost", 4.1f, 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(); lieGhostClipper = lieGhostText.gameObject.AddComponent(); actorFlashClipper = actorFlashText.gameObject.AddComponent(); actorFlashChromaRedClipper = actorFlashChromaRed.gameObject.AddComponent(); actorFlashChromaCyanClipper = actorFlashChromaCyan.gameObject.AddComponent(); truthFlashClipper = truthFlashText.gameObject.AddComponent(); lieClipper.Initialize(lieText, expressionScreenMaskRect); lieGhostClipper.Initialize(lieGhostText, expressionScreenMaskRect); actorFlashClipper.Initialize(actorFlashText, expressionScreenMaskRect); actorFlashChromaRedClipper.Initialize(actorFlashChromaRed, expressionScreenMaskRect); actorFlashChromaCyanClipper.Initialize(actorFlashChromaCyan, expressionScreenMaskRect); truthFlashClipper.Initialize(truthFlashText, expressionScreenMaskRect); PositionScreenText(); GameObject scanlineObject = new GameObject("ScreenScanline"); scanlineObject.transform.SetParent(runtimeRoot, false); scanlineRenderer = scanlineObject.AddComponent(); scanlineRenderer.sprite = runtimeWhiteSprite; scanlineRenderer.color = new Color(0.55f, 0.95f, 1f, 0f); scanlineRenderer.sortingLayerID = GetScreenSortingLayerId(); scanlineRenderer.sortingOrder = screenTextSortingOrder - 2; scanlineRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; scanlineRenderer.enabled = false; GameObject dimObject = new GameObject("ScreenBrightnessDip"); dimObject.transform.SetParent(runtimeRoot, false); screenDimRenderer = dimObject.AddComponent(); screenDimRenderer.sprite = runtimeWhiteSprite; screenDimRenderer.color = new Color(0.02f, 0.05f, 0.08f, 0f); screenDimRenderer.sortingLayerID = GetScreenSortingLayerId(); screenDimRenderer.sortingOrder = screenGlowSortingOrder + 1; screenDimRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; Bounds screen = GetScreenBounds(); screenDimRenderer.transform.position = screen.center; screenDimRenderer.transform.localScale = new Vector3(screen.size.x, screen.size.y, 1f); screenDimRenderer.enabled = false; GameObject backdropObject = new GameObject("TruthLieFlickerBackdrop"); backdropObject.transform.SetParent(runtimeRoot, false); flickerBackdropRenderer = backdropObject.AddComponent(); flickerBackdropRenderer.sprite = runtimeWhiteSprite; flickerBackdropRenderer.color = lieBackdropColor; flickerBackdropRenderer.sortingLayerID = GetScreenSortingLayerId(); // 底色在记忆/脸之上、屏幕暗脉冲(glow+1)与文字之下,顿挫黑场能盖住它 flickerBackdropRenderer.sortingOrder = screenGlowSortingOrder; flickerBackdropRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; flickerBackdropRenderer.transform.position = screen.center; flickerBackdropRenderer.transform.localScale = new Vector3(screen.size.x, screen.size.y, 1f); flickerBackdropRenderer.enabled = false; backdropObject.SetActive(false); runtimeObjectsReady = true; } private SpriteRenderer CreateMemoryRenderer(string objectName) { GameObject obj = new GameObject(objectName); obj.transform.SetParent(runtimeRoot, false); // 与粒子 / 屏幕同属 HuoshanScreen,进入 VolFx huoshan 合成; // 留在 Default 会被 sortingOrder 10 的不透明 noise 盖住。 int huoshanScreen = LayerMask.NameToLayer("HuoshanScreen"); if (huoshanScreen >= 0) obj.layer = huoshanScreen; SpriteRenderer renderer = obj.AddComponent(); renderer.sharedMaterial = memoryMaterial; renderer.sortingOrder = 0; renderer.maskInteraction = SpriteMaskInteraction.None; renderer.enabled = false; obj.SetActive(false); return renderer; } private TextMeshPro CreateScreenText(string objectName, float fontSize, int sortingOrder) { GameObject obj = new GameObject(objectName); obj.transform.SetParent(runtimeRoot, false); RectTransform rect = obj.AddComponent(); TextMeshPro text = obj.AddComponent(); text.font = screenFont != null ? screenFont : TMP_Settings.defaultFontAsset; text.fontSize = fontSize; text.fontStyle = FontStyles.Normal; text.enableAutoSizing = false; text.enableWordWrapping = false; text.alignment = TextAlignmentOptions.Center; text.overflowMode = TextOverflowModes.Overflow; text.sortingLayerID = GetScreenSortingLayerId(); text.sortingOrder = sortingOrder; text.text = string.Empty; obj.SetActive(false); return text; } private void CreateScreenMask() { GameObject maskObject = new GameObject("ExpressionScreenSpriteMask"); maskObject.transform.SetParent(runtimeRoot, false); screenSpriteMask = maskObject.AddComponent(); screenSpriteMask.sprite = runtimeWhiteSprite; screenSpriteMask.alphaCutoff = 0.01f; screenSpriteMask.isCustomRangeActive = true; int sortingLayer = GetScreenSortingLayerId(); screenSpriteMask.frontSortingLayerID = sortingLayer; screenSpriteMask.frontSortingOrder = screenTextSortingOrder + 2; screenSpriteMask.backSortingLayerID = sortingLayer; screenSpriteMask.backSortingOrder = screenGlowSortingOrder - 1; AlignMaskToScreenRect(maskObject.transform); } private void PositionScreenText() { Bounds screen = GetScreenBounds(); Vector3 center = screen.center; if (faceRenderer != null) center.z = faceRenderer.transform.position.z; PositionText(lieText, center, screen.size); PositionText(lieGhostText, center, screen.size); PositionText(actorFlashText, center, screen.size); PositionText(actorFlashChromaRed, center, screen.size); PositionText(actorFlashChromaCyan, center, screen.size); PositionText(truthFlashText, center, screen.size); } private void EnsureScreenTextClipMaterials() { lieClipper?.Initialize(lieText, expressionScreenMaskRect); lieGhostClipper?.Initialize(lieGhostText, expressionScreenMaskRect); actorFlashClipper?.Initialize(actorFlashText, expressionScreenMaskRect); actorFlashChromaRedClipper?.Initialize(actorFlashChromaRed, expressionScreenMaskRect); actorFlashChromaCyanClipper?.Initialize(actorFlashChromaCyan, expressionScreenMaskRect); truthFlashClipper?.Initialize(truthFlashText, expressionScreenMaskRect); foreach (TMPRectClipper clipper in actorScrollClippers) clipper?.Initialize(clipper.GetComponent(), expressionScreenMaskRect); } private void ReleaseScreenTextClipMaterials() { lieClipper?.ReleaseMaterial(); lieGhostClipper?.ReleaseMaterial(); actorFlashClipper?.ReleaseMaterial(); actorFlashChromaRedClipper?.ReleaseMaterial(); actorFlashChromaCyanClipper?.ReleaseMaterial(); truthFlashClipper?.ReleaseMaterial(); foreach (TMPRectClipper clipper in actorScrollClippers) clipper?.ReleaseMaterial(); } private static void PositionText(TextMeshPro text, Vector3 position, Vector2 size) { if (text == null) return; text.transform.position = position; RectTransform rect = text.rectTransform; rect.sizeDelta = size; rect.localScale = Vector3.one; } private void AlignMaskToScreenRect(Transform maskTransform) { Bounds screen = GetScreenBounds(); maskTransform.position = screen.center; maskTransform.rotation = expressionScreenMaskRect != null ? expressionScreenMaskRect.rotation : Quaternion.identity; maskTransform.localScale = new Vector3(screen.size.x, screen.size.y, 1f); } /// /// 记忆图作为全屏背景:按主相机视野 cover 缩放(铺满、超出部分裁掉), /// 渲染顺序在脸后面(sortingOrder 0 vs 脸的 2),透过脸的透明区域露出。 /// private void FitMemoryToView(SpriteRenderer renderer) { if (renderer == null || renderer.sprite == null) return; Vector2 viewportSize; Vector3 viewportCenter; Quaternion viewportRotation = Quaternion.identity; Camera viewCamera = Camera.main; if (viewCamera != null) { float planeZ = renderer.transform.position.z; float height = viewCamera.orthographic ? viewCamera.orthographicSize * 2f : 2f * Mathf.Abs(planeZ - viewCamera.transform.position.z) * Mathf.Tan(viewCamera.fieldOfView * 0.5f * Mathf.Deg2Rad); viewportSize = new Vector2(height * viewCamera.aspect, height); viewportCenter = new Vector3( viewCamera.transform.position.x, viewCamera.transform.position.y, planeZ); } else if (faceRenderer != null && faceRenderer.sprite != null) { Vector3 faceScale = faceRenderer.transform.lossyScale; Vector3 faceSpriteSize = faceRenderer.sprite.bounds.size; viewportSize = new Vector2( Mathf.Abs(faceSpriteSize.x * faceScale.x), Mathf.Abs(faceSpriteSize.y * faceScale.y)); viewportCenter = faceRenderer.transform.position; viewportRotation = faceRenderer.transform.rotation; } else { Bounds screen = GetScreenBounds(); float height = screen.size.y * 2.4f; viewportSize = new Vector2(height * 16f / 9f, height); viewportCenter = screen.center; } Vector2 spriteSize = renderer.sprite.bounds.size; float uniformScale = Mathf.Max( viewportSize.x / Mathf.Max(0.001f, spriteSize.x), viewportSize.y / Mathf.Max(0.001f, spriteSize.y)); renderer.transform.SetPositionAndRotation(viewportCenter, viewportRotation); renderer.transform.localScale = Vector3.one * uniformScale; } private void ApplyMemoryBlocks() { ApplyMemoryBlock(memoryRendererA, memoryBlockA, memoryOpacityA); ApplyMemoryBlock(memoryRendererB, memoryBlockB, memoryOpacityB); } private void ApplyMemoryBlock(SpriteRenderer renderer, MaterialPropertyBlock block, float opacity) { if (renderer == null || block == null) return; renderer.GetPropertyBlock(block); block.SetFloat(SaturationId, saturation); block.SetFloat(BrightnessId, brightness); block.SetFloat(ContrastId, contrast); block.SetFloat(VignetteId, vignette); block.SetFloat(NoiseId, noise); // 模拟感随记忆自身透明度浮现/退场,避免淡入期间闪线突兀。 block.SetFloat(SimGlitchId, simGlitch * opacity); block.SetFloat(HorizontalTearId, horizontalTear); block.SetFloat(MemoryImpactId, memoryImpact); block.SetFloat(MemoryDamageId, memoryDamage); block.SetFloat(MemoryOverdriveId, memoryOverdrive); block.SetFloat(MemoryTearSeedId, memoryTearSeed); block.SetColor(ColorTintId, memoryTint); block.SetVector(CalmCenterId, new Vector4(calmCenter.x, calmCenter.y, 0f, 0f)); block.SetFloat(CalmRadiusId, calmRadius); block.SetFloat(CalmFeatherId, calmFeather); block.SetFloat(EffectStrengthId, effectStrength); block.SetFloat(OpacityId, opacity); renderer.SetPropertyBlock(block); } private void TweenMemoryOpacity( SpriteRenderer renderer, float target, float duration, Ease ease = Ease.OutQuad) { if (renderer == null) return; DOTween.Kill(renderer); DOTween.To( () => GetMemoryOpacity(renderer), value => SetMemoryOpacity(renderer, value), Mathf.Clamp01(target), Mathf.Max(0.01f, duration)) .SetEase(ease) .SetTarget(renderer); } private float GetMemoryOpacity(SpriteRenderer renderer) { return renderer == memoryRendererA ? memoryOpacityA : memoryOpacityB; } private void SetMemoryOpacity(SpriteRenderer renderer, float opacity) { if (renderer == memoryRendererA) memoryOpacityA = opacity; else if (renderer == memoryRendererB) memoryOpacityB = opacity; ApplyMemoryBlocks(); } private void DisableMemoryRenderer(SpriteRenderer renderer) { if (renderer == null) return; DOTween.Kill(renderer); renderer.enabled = false; renderer.gameObject.SetActive(false); renderer.sprite = null; } private void PlayScanline(float duration, bool stalled) { if (scanlineRenderer == null) return; Bounds screen = GetScreenBounds(); scanlineRenderer.enabled = true; scanlineRenderer.gameObject.SetActive(true); scanlineRenderer.transform.localScale = new Vector3(screen.size.x, 0.035f, 1f); float y = stalled ? screen.center.y : screen.max.y; scanlineRenderer.transform.position = new Vector3(screen.center.x, y, screen.center.z); scanlineRenderer.color = new Color(0.55f, 0.95f, 1f, stalled ? 0.72f : 0.62f); scanlineRenderer.transform.DOKill(); if (!stalled) { scanlineRenderer.transform.DOMoveY(screen.min.y, duration) .SetEase(Ease.Linear) .SetTarget(this) .OnComplete(StopScanline); } else { scanlineRenderer.DOFade(0.15f, 0.055f) .SetLoops(4, LoopType.Yoyo) .SetTarget(this); } } private void StopScanline() { if (scanlineRenderer == null) return; scanlineRenderer.transform.DOKill(); scanlineRenderer.DOKill(); scanlineRenderer.enabled = false; scanlineRenderer.gameObject.SetActive(false); } private void PulseScreenDim(float alpha, float riseDuration, float fallDuration) { if (screenDimRenderer == null) return; screenDimRenderer.DOKill(); screenDimRenderer.enabled = true; screenDimRenderer.gameObject.SetActive(true); screenDimRenderer.color = new Color(0.02f, 0.05f, 0.08f, 0f); Sequence sequence = DOTween.Sequence().SetTarget(this); sequence.Append(screenDimRenderer.DOFade(Mathf.Clamp01(alpha), Mathf.Max(0.01f, riseDuration))); sequence.Append(screenDimRenderer.DOFade(0f, Mathf.Max(0.01f, fallDuration))); sequence.OnComplete(StopScreenDim); } private void PulseTruthBreakFlash( float alpha, float riseDuration, float holdDuration, float fallDuration) { if (screenDimRenderer == null) return; screenDimRenderer.DOKill(); screenDimRenderer.enabled = true; screenDimRenderer.gameObject.SetActive(true); screenDimRenderer.color = new Color( truthWarmColor.r, truthWarmColor.g, truthWarmColor.b, 0f); Sequence sequence = DOTween.Sequence().SetTarget(this); sequence.Append(screenDimRenderer.DOFade(Mathf.Clamp01(alpha), Mathf.Max(0.01f, riseDuration))); sequence.AppendInterval(Mathf.Max(0f, holdDuration)); sequence.Append(screenDimRenderer.DOFade(0f, Mathf.Max(0.01f, fallDuration))); sequence.OnComplete(StopScreenDim); } private void PulseTruthAttackFlash(float alpha, float riseDuration, float fallDuration) { if (screenDimRenderer == null) return; screenDimRenderer.DOKill(); screenDimRenderer.enabled = true; screenDimRenderer.gameObject.SetActive(true); screenDimRenderer.color = new Color( truthAttackFlashColor.r, truthAttackFlashColor.g, truthAttackFlashColor.b, 0f); Sequence sequence = DOTween.Sequence().SetTarget(this); sequence.Append(screenDimRenderer.DOFade( Mathf.Clamp01(alpha), Mathf.Max(0.01f, riseDuration))); sequence.Append(screenDimRenderer.DOFade( 0f, Mathf.Max(0.01f, fallDuration))); sequence.OnComplete(StopScreenDim); } private void HoldTruthBreakBlackout(float alpha) { if (screenDimRenderer == null) return; screenDimRenderer.DOKill(); screenDimRenderer.enabled = true; screenDimRenderer.gameObject.SetActive(true); screenDimRenderer.color = new Color(0.01f, 0.025f, 0.04f, Mathf.Clamp01(alpha)); } private void PulseMemoryRelease(float amount, float riseDuration, float fallDuration) { float originalBrightness = brightness; Sequence sequence = DOTween.Sequence().SetTarget(this); sequence.Append(DOTween.To( () => brightness, value => brightness = value, originalBrightness + amount, Mathf.Max(0.01f, riseDuration))); sequence.Append(DOTween.To( () => brightness, value => brightness = value, originalBrightness, Mathf.Max(0.01f, fallDuration))); } private void StopScreenDim() { if (screenDimRenderer == null) return; screenDimRenderer.DOKill(); screenDimRenderer.enabled = false; screenDimRenderer.gameObject.SetActive(false); } private void HideLieLayersImmediate() { StopLieShuffleImmediate(false); HideTextImmediate(lieText); HideTextImmediate(lieGhostText); HideActorFlashImmediate(); } private void HideActorFlashImmediate() { StopActorOverdriveImmediate(); StopTruthLieFlicker(); actorFlashActive = false; actorFlashCount = 0; memoryImpact = 0f; memoryDamage = 0f; memoryOverdrive = 0f; HideActorFlashVisualsImmediate(); HideActorScrollImmediate(); } private void HideActorFlashVisualsImmediate() { if (actorFlashText != null) actorFlashText.fontSize = 10.5f; if (actorFlashChromaRed != null) actorFlashChromaRed.fontSize = 10.5f; if (actorFlashChromaCyan != null) actorFlashChromaCyan.fontSize = 10.5f; HideTextImmediate(actorFlashText); HideTextImmediate(actorFlashChromaRed); HideTextImmediate(actorFlashChromaCyan); truthFlashActive = false; HideTextImmediate(truthFlashText); } private static void HideTextImmediate(TextMeshPro text) { if (text == null) return; text.DOKill(); text.text = string.Empty; text.gameObject.SetActive(false); } private static void SetTextActive(TextMeshPro text, string value, Color color) { if (text == null) return; text.gameObject.SetActive(true); text.enabled = true; text.text = value ?? string.Empty; text.color = color; } private static void SetTextAlpha(TextMeshPro text, float alpha) { if (text == null) return; Color color = text.color; color.a = Mathf.Clamp01(alpha); text.color = color; } private void FadeText(TextMeshPro text, float alpha, float duration) { if (text == null || !text.gameObject.activeSelf) return; text.DOFade(alpha, duration).SetEase(Ease.OutQuad).SetTarget(this); } private Bounds GetScreenBounds() { if (expressionScreenMaskRect == null) return particleManager != null ? particleManager.GetScreenWorldBounds() : new Bounds(transform.position, new Vector3(5.77f, 4.2f, 0f)); Vector3[] corners = new Vector3[4]; expressionScreenMaskRect.GetWorldCorners(corners); Bounds result = new Bounds(corners[0], Vector3.zero); for (int i = 1; i < corners.Length; i++) result.Encapsulate(corners[i]); return result; } private int GetScreenSortingLayerId() { Canvas canvas = expressionScreenMaskRect != null ? expressionScreenMaskRect.GetComponentInParent() : null; return canvas != null ? canvas.sortingLayerID : 0; } private bool TryResolveMemory(string key, out Sprite sprite, out MemoryKind kind) { switch ((key ?? string.Empty).Trim().ToLowerInvariant()) { case "office": sprite = officeMemory; kind = MemoryKind.Office; return sprite != null; case "sunset": sprite = sunsetMemory; kind = MemoryKind.Sunset; return sprite != null; case "private_office": sprite = privateOfficeMemory; kind = MemoryKind.PrivateOffice; return sprite != null; default: sprite = null; kind = MemoryKind.Office; return false; } } private static Preset GetPresetForMemory(MemoryKind kind) { return kind switch { MemoryKind.Sunset => MediumPreset, MemoryKind.PrivateOffice => HeavyPreset, _ => LightPreset }; } private bool ExpectState(string command, params PresentationState[] allowed) { for (int i = 0; i < allowed.Length; i++) { if (state == allowed[i]) return true; } Debug.LogWarning( $"[LogReleasePresentation] {command} 在状态 {state} 被调用;已安全跳过,不阻塞 Yarn。"); return false; } private void OnDestroy() { DOTween.Kill(this); if (runtimeWhiteSprite != null) Destroy(runtimeWhiteSprite); if (runtimeWhiteTexture != null) Destroy(runtimeWhiteTexture); } } }