Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs
T

2939 lines
121 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using DG.Tweening;
using TMPro;
using UnityEngine;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// Owns the post-output performance for Huoshan's three blocked logs. All memory
/// distortion lives on the HuoshanMemory material; face / dialogue stay clean.
/// </summary>
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 readonly float LiePause;
public readonly float BreakPeak;
public readonly float BreakDuration;
public readonly float TruthDuration;
public readonly float TruthPeripheral;
public Preset(
string name,
float baseDistortion,
float liePause,
float breakPeak,
float breakDuration,
float truthDuration,
float truthPeripheral)
{
Name = name;
BaseDistortion = baseDistortion;
LiePause = liePause;
BreakPeak = breakPeak;
BreakDuration = breakDuration;
TruthDuration = truthDuration;
TruthPeripheral = truthPeripheral;
}
}
private static readonly Preset LightPreset = new Preset("Light", 0.10f, 0.35f, 0.25f, 0.25f, 0.70f, 0.15f);
private static readonly Preset MediumPreset = new Preset("Medium", 0.20f, 0.55f, 0.50f, 0.35f, 0.90f, 0.30f);
private static readonly Preset HeavyPreset = new Preset("Heavy", 0.35f, 0.80f, 0.85f, 0.50f, 1.10f, 0.50f);
[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 errorColor = new Color(1f, 0.34f, 0.18f, 1f);
[SerializeField] private Color truthWarmColor = new Color(1f, 1f, 1f, 1f);
[Tooltip("爆发段真话闪现与老虎机真话字符的暖色;需与谎话的冷白明显区分。")]
[SerializeField] private Color truthAccentColor = new Color(1f, 0.84f, 0.6f, 1f);
[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 string currentLeakFragment = 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 systemText;
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 systemClipper;
private TMPRectClipper actorFlashClipper;
private TMPRectClipper actorFlashChromaRedClipper;
private TMPRectClipper actorFlashChromaCyanClipper;
private readonly List<TextMeshPro> actorScrollTexts = new List<TextMeshPro>();
private readonly List<TMPRectClipper> actorScrollClippers = new List<TMPRectClipper>();
private bool actorFlashActive;
private int actorFlashCount;
private Coroutine actorOverdriveRoutine;
private Coroutine actorFlashLoopRoutine;
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(systemText, 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;
currentLeakFragment = GetLeakFragment(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);
}
/// <summary>把当前记忆透明度推进到目标值(非等待),用于随对白逐段显影。</summary>
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 IEnumerator BeginLie(string keyword, string presetName)
{
EnsureRuntimeObjects();
if (!TryResolvePreset(presetName, out Preset requestedPreset))
{
Debug.LogError($"[LogReleasePresentation] 未知演出预设“{presetName}”;命令安全结束。");
yield break;
}
if (!string.Equals(requestedPreset.Name, currentPreset.Name, StringComparison.OrdinalIgnoreCase))
{
Debug.LogWarning(
$"[LogReleasePresentation] expression_lie_begin 的预设 {requestedPreset.Name} " +
$"与当前记忆 {currentPreset.Name} 不一致,将沿用当前记忆预设。");
}
PrepareLieVisuals(0.25f, 0.005f);
SqueezeTruthCharacters(
0.22f,
0.055f + currentPreset.BreakPeak * 0.065f,
0.28f);
ShowLieKeyword(keyword, 0.22f, 14f);
PlayScreenScanline(0.32f, currentMemoryKind == MemoryKind.Sunset);
if (currentMemoryKind == MemoryKind.PrivateOffice)
ShowSystemMessage("输出正常", "normal", 0.12f);
else
HideSystemMessageImmediate();
yield return new WaitForSeconds(0.25f);
}
public IEnumerator BreakLie()
{
EnsureRuntimeObjects();
if (!ExpectState(nameof(BreakLie), PresentationState.LieHolding))
yield break;
yield return new WaitForSeconds(currentPreset.LiePause);
BeginLieCracking(0.48f);
PlayScreenScanline(0.22f, true);
PlayScreenDim(0.28f, 0.06f, 0.12f);
PlayMemoryJolt(0.08f, 0.24f, 0.11f);
yield return LeakTruthFragment(currentLeakFragment, 0.24f);
if (currentMemoryKind == MemoryKind.PrivateOffice)
{
HideSystemMessage(0.01f);
yield return new WaitForSeconds(0.08f);
ShowSystemMessage("输出与原始 log 不一致", "error", 0.08f);
yield return PulseGlitch(HeavyPreset.BreakPeak, 0.12f, 0.18f);
}
}
public IEnumerator RevealTruth()
{
EnsureRuntimeObjects();
if (!ExpectState(
nameof(RevealTruth),
PresentationState.LieCracking,
PresentationState.LieHolding))
{
yield break;
}
DOTween.Kill(this);
yield return HoldTruthBlackout(0.055f, 0.42f);
SplitLieVisual(currentPreset.BreakDuration, 0.18f, 1.85f, 0.42f);
PlayTruthFlash(0.62f, 0.045f, 0.025f, 0.16f);
PlayMemoryBrightnessPulse(0.16f, 0.06f, 0.20f);
PlayMemoryTear(currentPreset.BreakPeak, currentPreset.BreakDuration);
PlayGlitchPulse(currentPreset.BreakPeak, 0.12f, currentPreset.BreakDuration);
PlayFaceDip(currentPreset.BreakDuration, faceDipRatio);
ReleaseTruthCharacters(0.08f);
yield return ResolveTruthCharacters(
currentPreset.TruthDuration,
currentPreset.TruthPeripheral,
0.42f);
}
public void PrepareLieVisuals(float freezeDuration, float noiseTarget)
{
EnsureRuntimeObjects();
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);
}
/// <summary>调节记忆材质的轻微失真扰动(非等待)。</summary>
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 ShowSystemMessage(string value, string style, float fadeDuration)
{
EnsureRuntimeObjects();
if (!ExpectState(
nameof(ShowSystemMessage),
PresentationState.FaceEntering,
PresentationState.Memory,
PresentationState.LieHolding,
PresentationState.LieCracking))
return;
Color color;
if (string.Equals(style, "error", StringComparison.OrdinalIgnoreCase))
{
color = errorColor;
}
else
{
if (!string.Equals(style, "normal", StringComparison.OrdinalIgnoreCase))
Debug.LogWarning($"[LogReleasePresentation] 未知系统提示样式“{style}”,改用 normal。");
color = new Color(0.58f, 0.78f, 0.82f, 1f);
}
SetSystemMessage(value ?? string.Empty, color, Mathf.Max(0.01f, fadeDuration));
}
public void HideSystemMessage(float fadeDuration)
{
if (systemText == null || !systemText.gameObject.activeSelf)
return;
systemText.DOKill();
systemText.DOFade(0f, Mathf.Max(0.01f, fadeDuration))
.SetEase(Ease.OutQuad)
.SetTarget(this)
.OnComplete(HideSystemMessageImmediate);
}
public void BeginLieCracking(float lieAlpha)
{
if (!ExpectState(
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;
currentLeakFragment = fragment ?? string.Empty;
yield return ShuffleLieCharactersRoutine(
currentLeakFragment,
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);
StopActorFlashLoopImmediate(false);
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();
}
/// <summary>
/// 演员大字闪现:glitch 一拍的瞬间隐藏抖动的粒子字,屏幕中央出现白色大字。
/// 三次冲击逐级增强并累积记忆损伤;普通闪现结束后始终退回抖动粒子字。
/// 只有 expression_actor_flash_in 会保持大字,用于明确的结尾定格。
/// </summary>
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);
}
}
public void StartActorFlashLoop()
{
EnsureRuntimeObjects();
if (!ExpectState(nameof(StartActorFlashLoop), PresentationState.Memory))
return;
StopActorFlashLoopImmediate(true);
actorFlashLoopRoutine = StartCoroutine(ActorFlashLoopRoutine());
}
public void StopActorFlashLoop()
{
StopActorFlashLoopImmediate(true);
}
private IEnumerator ActorFlashLoopRoutine()
{
while (state == PresentationState.Memory)
{
yield return FlashActorLie(
"没问题", 0.25f, 10.5f, 1.1f, 1.45f, 0.008f, 0.06f, 0.50f, 0f);
if (state != PresentationState.Memory)
break;
yield return new WaitForSeconds(0.3f);
yield return FlashActorLie(
"冇问题", 0.25f, 11.2f, 1.1f, 1.85f, 0.008f, 0.06f, 0.72f, 0f);
if (state != PresentationState.Memory)
break;
yield return new WaitForSeconds(0.3f);
yield return FlashActorLie(
"帽问题", 0.15f, 12.0f, 1.1f, 2.20f, 0.010f, 0.08f, 1.00f, 0f);
if (state != PresentationState.Memory)
break;
yield return new WaitForSeconds(0.3f);
}
actorFlashLoopRoutine = null;
}
private void StopActorFlashLoopImmediate(bool restoreParticles)
{
if (actorFlashLoopRoutine != null)
{
StopCoroutine(actorFlashLoopRoutine);
actorFlashLoopRoutine = null;
}
actorFlashActive = false;
memoryImpact = 0f;
HideActorFlashVisualsImmediate();
if (restoreParticles && state == PresentationState.Memory)
particleManager?.SetTargetParticlesAlpha(1f, 0f);
}
/// <summary>
/// 真话闪现:谎话大字之间,暖色完整真话带着细微抖动挣扎浮现一拍。
/// 不推进谎话冲击等级、不累积记忆损伤;"被掐断"由紧随其后的 expression_glitch_pulse 表现。
/// </summary>
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);
}
}
/// <summary>
/// 老虎机换字演出:聚焦字在 duration 内只从 charPool 里疯狂换字(非阻塞,节拍由 Yarn wait 控制)。
/// truthPool 非空时其字符也混入换字池,并在主字上以真话暖色显示——真假字符交替闪烁。
/// </summary>
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();
HideSystemMessageImmediate();
StopScanline();
StopScreenDim();
particleManager?.PrepareTruthReleaseFromLie();
particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor);
particleManager?.SetTargetParticlesAlpha(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;
}
/// <summary>真话聚合尝试(非等待):目标字被慢慢拉向最终位置并按序逐字锁定,感觉即将成功。</summary>
public void BeginTruthGatherAttempt(float target, float duration)
{
if (!ExpectState(nameof(BeginTruthGatherAttempt), PresentationState.Memory))
return;
particleManager?.BeginTruthGather(target, duration);
}
/// <summary>真话聚合被打断:目标字向外爆散并恢复乱跳(非等待)。</summary>
public void ScatterTruthGatherAttempt(float burstDistance)
{
if (!ExpectState(nameof(ScatterTruthGatherAttempt), PresentationState.Memory))
return;
particleManager?.ScatterTruthGather(burstDistance);
}
/// <summary>真话聚合挣扎(非等待):推进途中反复"够到又被拽回去",字会锁上再崩开。</summary>
public void BeginTruthStrainAttempt(float target, float duration, int tugs)
{
if (!ExpectState(nameof(BeginTruthStrainAttempt), PresentationState.Memory))
return;
particleManager?.BeginTruthStrain(target, duration, tugs);
}
/// <summary>真话聚合崩塌(非等待):进度被拽回 0,所有字沿原路退回并恢复乱跳。</summary>
public void CollapseTruthGatherAttempt(float duration)
{
if (!ExpectState(nameof(CollapseTruthGatherAttempt), PresentationState.Memory))
return;
particleManager?.CollapseTruthGather(duration);
}
/// <summary>火山脸错误闪红:聚合失败的报错信号(非等待)。</summary>
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;
}
/// <summary>
/// 结尾真假快速闪切(非等待):真心话与谎话交替独占屏幕,两者的屏幕底色(电视头背景)不同,
/// 越到后面切得越快,最后定格在真话帧。节奏由同长度的 Yarn wait 控制。
/// </summary>
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();
}
/// <summary>
/// 定格的谎话被 glitch 翻转成真话(等待):大字在乱跳中长度渐变、真假底色抢闪,
/// 最后落在暖色真话并轻微收缩定住。后续由 expression_memory_cut + expression_truth_snap 收尾。
/// </summary>
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));
}
/// <summary>
/// 记忆材质上的瞬时失真脉冲(抬升 _SimGlitch 后回落),不再驱动 noise 叠层。
/// 撕裂仍走 PlayMemoryTear / impact,避免两路抢同一参数。
/// </summary>
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)));
}
/// <summary>
/// 已弃用:屏幕 Glitch 请走 ExpressionManager.SetScreenGlitchLevel(屏幕自身材质)。
/// 记忆阶段失真请用 PlayGlitchPulse / 记忆 impact。
/// </summary>
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;
HideSystemMessage(0.15f);
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();
HideSystemMessageImmediate();
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();
HideSystemMessageImmediate();
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();
actorFlashLoopRoutine = null;
RestoreFaceSpriteImmediate();
memoryOpacityA = 0f;
memoryOpacityB = 0f;
DisableMemoryRenderer(memoryRendererA);
DisableMemoryRenderer(memoryRendererB);
activeMemoryRenderer = null;
HideLieLayersImmediate();
HideSystemMessageImmediate();
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;
currentLeakFragment = 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("<color=#").Append(colorHex).Append('>').Append(character).Append("</color>");
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>();
string[] raw = phraseList.Split('|');
var result = new List<string>(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<TMPRectClipper>();
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();
}
/// <summary>记忆材质瞬时失真:抬升 _SimGlitch 后回到基线。</summary>
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);
}
/// <summary>
/// 进记忆交接:把进场前的 noise 从「不透明黑底」切成透明叠层并淡出,
/// 让记忆材质自身的 _SimGlitch / Analog 接上失真,而不是突然掐断或继续盖住画面。
/// </summary>
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<LanguageParticleManager>(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);
systemText = CreateScreenText("SystemPrompt", 1.35f, screenTextSortingOrder + 1);
actorFlashText = CreateScreenText("ActorFlashKeyword", 10.5f, screenTextSortingOrder);
actorFlashChromaRed = CreateScreenText("ActorFlashChromaRed", 10.5f, screenTextSortingOrder - 1);
actorFlashChromaCyan = CreateScreenText("ActorFlashChromaCyan", 10.5f, screenTextSortingOrder - 1);
truthFlashText = CreateScreenText("ActorTruthFlash", 8.5f, screenTextSortingOrder - 1);
lieClipper = lieText.gameObject.AddComponent<TMPRectClipper>();
lieGhostClipper = lieGhostText.gameObject.AddComponent<TMPRectClipper>();
systemClipper = systemText.gameObject.AddComponent<TMPRectClipper>();
actorFlashClipper = actorFlashText.gameObject.AddComponent<TMPRectClipper>();
actorFlashChromaRedClipper = actorFlashChromaRed.gameObject.AddComponent<TMPRectClipper>();
actorFlashChromaCyanClipper = actorFlashChromaCyan.gameObject.AddComponent<TMPRectClipper>();
truthFlashClipper = truthFlashText.gameObject.AddComponent<TMPRectClipper>();
lieClipper.Initialize(lieText, expressionScreenMaskRect);
lieGhostClipper.Initialize(lieGhostText, expressionScreenMaskRect);
systemClipper.Initialize(systemText, expressionScreenMaskRect);
actorFlashClipper.Initialize(actorFlashText, expressionScreenMaskRect);
actorFlashChromaRedClipper.Initialize(actorFlashChromaRed, expressionScreenMaskRect);
actorFlashChromaCyanClipper.Initialize(actorFlashChromaCyan, expressionScreenMaskRect);
truthFlashClipper.Initialize(truthFlashText, expressionScreenMaskRect);
PositionScreenText();
GameObject scanlineObject = new GameObject("ScreenScanline");
scanlineObject.transform.SetParent(runtimeRoot, false);
scanlineRenderer = scanlineObject.AddComponent<SpriteRenderer>();
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<SpriteRenderer>();
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<SpriteRenderer>();
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<SpriteRenderer>();
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<RectTransform>();
TextMeshPro text = obj.AddComponent<TextMeshPro>();
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<SpriteMask>();
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);
Vector3 systemPosition = center + Vector3.up * screen.extents.y * 0.70f;
PositionText(systemText, systemPosition, new Vector2(screen.size.x, screen.size.y * 0.22f));
}
private void EnsureScreenTextClipMaterials()
{
lieClipper?.Initialize(lieText, expressionScreenMaskRect);
lieGhostClipper?.Initialize(lieGhostText, expressionScreenMaskRect);
systemClipper?.Initialize(systemText, expressionScreenMaskRect);
actorFlashClipper?.Initialize(actorFlashText, expressionScreenMaskRect);
actorFlashChromaRedClipper?.Initialize(actorFlashChromaRed, expressionScreenMaskRect);
actorFlashChromaCyanClipper?.Initialize(actorFlashChromaCyan, expressionScreenMaskRect);
truthFlashClipper?.Initialize(truthFlashText, expressionScreenMaskRect);
foreach (TMPRectClipper clipper in actorScrollClippers)
clipper?.Initialize(clipper.GetComponent<TextMeshPro>(), expressionScreenMaskRect);
}
private void ReleaseScreenTextClipMaterials()
{
lieClipper?.ReleaseMaterial();
lieGhostClipper?.ReleaseMaterial();
systemClipper?.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);
}
/// <summary>
/// 记忆图作为全屏背景:按主相机视野 cover 缩放(铺满、超出部分裁掉),
/// 渲染顺序在脸后面(sortingOrder 0 vs 脸的 2),透过脸的透明区域露出。
/// </summary>
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 SetSystemMessage(string value, Color color, float fadeDuration)
{
SetTextActive(systemText, value, color);
SetTextAlpha(systemText, 0f);
FadeText(systemText, 1f, fadeDuration);
}
private void HideSystemMessageImmediate()
{
if (systemText == null)
return;
systemText.DOKill();
systemText.text = string.Empty;
systemText.gameObject.SetActive(false);
}
private void HideLieLayersImmediate()
{
StopLieShuffleImmediate(false);
HideTextImmediate(lieText);
HideTextImmediate(lieGhostText);
HideActorFlashImmediate();
}
private void HideActorFlashImmediate()
{
StopActorFlashLoopImmediate(false);
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<Canvas>()
: 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 bool TryResolvePreset(string value, out Preset preset)
{
switch ((value ?? string.Empty).Trim().ToLowerInvariant())
{
case "light":
preset = LightPreset;
return true;
case "medium":
preset = MediumPreset;
return true;
case "heavy":
preset = HeavyPreset;
return true;
default:
preset = LightPreset;
return false;
}
}
private static Preset GetPresetForMemory(MemoryKind kind)
{
return kind switch
{
MemoryKind.Sunset => MediumPreset,
MemoryKind.PrivateOffice => HeavyPreset,
_ => LightPreset
};
}
private static string GetLeakFragment(MemoryKind kind)
{
return kind switch
{
MemoryKind.Office => "笑话",
MemoryKind.Sunset => "离开",
_ => "不要那样看我"
};
}
private bool ExpectState(string command, params PresentationState[] allowed)
{
for (int i = 0; i < allowed.Length; i++)
{
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);
}
}
}