2686 lines
111 KiB
C#
2686 lines
111 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 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<TextMeshPro> actorScrollTexts = new List<TextMeshPro>();
|
|
private readonly List<TMPRectClipper> actorScrollClippers = new List<TMPRectClipper>();
|
|
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);
|
|
}
|
|
|
|
/// <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 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 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();
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
|
|
/// <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();
|
|
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;
|
|
}
|
|
|
|
/// <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;
|
|
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("<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);
|
|
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>();
|
|
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);
|
|
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);
|
|
}
|
|
|
|
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<TextMeshPro>(), 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);
|
|
}
|
|
|
|
/// <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 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<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 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);
|
|
}
|
|
}
|
|
}
|