2229 lines
90 KiB
C#
2229 lines
90 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using DG.Tweening;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
|
|
namespace AibisDream.MiniGame.Language
|
|
{
|
|
/// <summary>
|
|
/// Owns the post-output performance for Huoshan's three blocked logs. Memory effects
|
|
/// stay on their own SpriteRenderers, while the opaque face and dialogue UI remain 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 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("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, 0.84f, 0.64f, 1f);
|
|
[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 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 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 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 memoryMotionWeight;
|
|
private float memoryMotionTime;
|
|
private float memoryPushSpeed;
|
|
private float memoryHorizontalSpeed;
|
|
private float memoryZoom = 1f;
|
|
private Vector3 memoryBasePosition;
|
|
private Vector3 memoryBaseScale;
|
|
private Vector3 faceBasePosition;
|
|
private bool runtimeObjectsReady;
|
|
private Coroutine lieShuffleRoutine;
|
|
|
|
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 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;
|
|
ForceCleanupImmediate(true);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!runtimeObjectsReady || activeMemoryRenderer == null || !activeMemoryRenderer.enabled)
|
|
return;
|
|
|
|
if (memoryMotionWeight > 0.001f)
|
|
{
|
|
memoryMotionTime += Time.deltaTime * memoryMotionWeight;
|
|
float scale = (1f + memoryMotionTime * memoryPushSpeed) * memoryZoom;
|
|
activeMemoryRenderer.transform.localScale = memoryBaseScale * scale;
|
|
|
|
float horizontal = memoryMotionTime * memoryHorizontalSpeed;
|
|
activeMemoryRenderer.transform.position = memoryBasePosition + Vector3.left * horizontal;
|
|
|
|
if (currentMemoryKind == MemoryKind.Sunset)
|
|
saturation = Mathf.Max(0.58f, saturation - Time.deltaTime * 0.028f * memoryMotionWeight);
|
|
else if (currentMemoryKind == MemoryKind.PrivateOffice)
|
|
brightness = Mathf.Max(0.62f, brightness - Time.deltaTime * 0.018f * memoryMotionWeight);
|
|
}
|
|
|
|
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) ||
|
|
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);
|
|
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;
|
|
if (glitchController != null)
|
|
{
|
|
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
|
|
glitchController.Intensity = 0f;
|
|
glitchController.SetAmbientNoise(0f, 320f, 0f);
|
|
}
|
|
|
|
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,
|
|
string presetName,
|
|
float fadeDuration = -1f,
|
|
float pushSpeed = -1f,
|
|
float horizontalSpeed = -1f,
|
|
bool preserveGlitch = false)
|
|
{
|
|
EnsureRuntimeObjects();
|
|
EnsureScreenTextClipMaterials();
|
|
if (!TryResolveMemory(memoryKey, out Sprite memorySprite, out MemoryKind kind))
|
|
{
|
|
Debug.LogError($"[LogReleasePresentation] 未知记忆图片“{memoryKey}”;命令安全结束。");
|
|
yield break;
|
|
}
|
|
if (!TryResolvePreset(presetName, out Preset preset))
|
|
{
|
|
Debug.LogError($"[LogReleasePresentation] 未知演出预设“{presetName}”;命令安全结束。");
|
|
yield break;
|
|
}
|
|
|
|
if (!ExpectState(
|
|
nameof(BeginMemory),
|
|
PresentationState.FaceEntering,
|
|
PresentationState.Memory,
|
|
PresentationState.TruthHolding))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
currentPreset = preset;
|
|
currentMemoryKind = kind;
|
|
currentLeakFragment = GetLeakFragment(kind);
|
|
actorFlashCount = 0;
|
|
memoryImpact = 0f;
|
|
memoryDamage = 0f;
|
|
memoryOverdrive = 0f;
|
|
memoryTearSeed = 0f;
|
|
state = PresentationState.Memory;
|
|
|
|
if (glitchController != null)
|
|
{
|
|
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
|
|
if (!preserveGlitch)
|
|
glitchController.Intensity = 0f;
|
|
glitchController.SetAmbientNoise(0f, 320f, 0f);
|
|
glitchController.ClearCalmField(true);
|
|
}
|
|
|
|
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);
|
|
memoryMotionTime = 0f;
|
|
memoryMotionWeight = 1f;
|
|
memoryZoom = 1f;
|
|
memoryPushSpeed = pushSpeed >= 0f
|
|
? pushSpeed
|
|
: kind == MemoryKind.PrivateOffice ? 0.040f : 0.022f;
|
|
memoryHorizontalSpeed = horizontalSpeed >= 0f
|
|
? horizontalSpeed
|
|
: kind == MemoryKind.Sunset ? 0.075f : 0f;
|
|
memoryBasePosition = incoming.transform.position;
|
|
memoryBaseScale = incoming.transform.localScale;
|
|
float resolvedFadeDuration = fadeDuration > 0f
|
|
? Mathf.Max(0.01f, fadeDuration)
|
|
: memoryFadeDuration;
|
|
SetMemoryOpacity(incoming, 0f);
|
|
TweenMemoryOpacity(incoming, 1f, resolvedFadeDuration);
|
|
if (outgoing != null && outgoing != incoming)
|
|
TweenMemoryOpacity(outgoing, 0f, resolvedFadeDuration);
|
|
|
|
yield return new WaitForSeconds(resolvedFadeDuration);
|
|
}
|
|
|
|
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);
|
|
DOTween.To(() => memoryMotionWeight, value => memoryMotionWeight = value, 0f, duration)
|
|
.SetEase(Ease.OutQuad)
|
|
.SetTarget(this);
|
|
DOTween.To(() => noise, value => noise = value, Mathf.Clamp01(noiseTarget), 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.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))
|
|
return;
|
|
|
|
DOTween.Kill(this);
|
|
StopActorOverdriveImmediate();
|
|
memoryMotionWeight = 0f;
|
|
memoryZoom = 1f;
|
|
memoryOpacityA = 0f;
|
|
memoryOpacityB = 0f;
|
|
horizontalTear = 0f;
|
|
noise = 0f;
|
|
effectStrength = 0f;
|
|
memoryImpact = 0f;
|
|
memoryDamage = 0f;
|
|
memoryOverdrive = 0f;
|
|
calmRadius = 0f;
|
|
DisableMemoryRenderer(memoryRendererA);
|
|
DisableMemoryRenderer(memoryRendererB);
|
|
activeMemoryRenderer = null;
|
|
|
|
if (glitchController != null)
|
|
{
|
|
glitchController.Intensity = 0f;
|
|
glitchController.SetAmbientNoise(0f, 320f, 0f);
|
|
glitchController.ClearCalmField(true);
|
|
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 演员大字闪现:glitch 一拍的瞬间隐藏抖动的粒子字,屏幕中央出现白色大字。
|
|
/// 三次冲击逐级增强并累积记忆损伤;第三次大字不再退回粒子,而是留给老虎机过载态接管。
|
|
/// </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)
|
|
{
|
|
EnsureRuntimeObjects();
|
|
EnsureScreenTextClipMaterials();
|
|
if (!ExpectState(nameof(FlashActorLie), PresentationState.Memory))
|
|
yield break;
|
|
if (actorFlashActive)
|
|
{
|
|
Debug.LogWarning("[LogReleasePresentation] expression_actor_flash 在上一次闪现结束前被调用;已安全跳过。");
|
|
yield break;
|
|
}
|
|
|
|
actorFlashActive = true;
|
|
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.22f, 0.36f, flashProgress);
|
|
float resolvedChromaDistance = chromaDistance >= 0f
|
|
? chromaDistance
|
|
: Mathf.Lerp(0.025f, 0.065f, 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 (glitchController != null)
|
|
{
|
|
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 = flashLevel >= 3 && state == PresentationState.Memory;
|
|
if (!holdForOverdrive)
|
|
HideActorFlashVisualsImmediate();
|
|
if (!holdForOverdrive && state == PresentationState.Memory)
|
|
particleManager?.SetTargetParticlesAlpha(1f, 0f);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 老虎机换字演出:聚焦字在 duration 内只从 charPool 里疯狂换字(非阻塞,节拍由 Yarn wait 控制)。
|
|
/// </summary>
|
|
public void StartActorSlotMachine(
|
|
string charPool,
|
|
float duration,
|
|
float interval,
|
|
float fontSize = 14f)
|
|
{
|
|
EnsureRuntimeObjects();
|
|
if (!ExpectState(nameof(StartActorSlotMachine), PresentationState.Memory))
|
|
return;
|
|
string pool = LanguageParticleManager.SanitizeSlotMachinePool(charPool);
|
|
if (string.IsNullOrEmpty(pool))
|
|
{
|
|
Debug.LogWarning("[LogReleasePresentation] 演员老虎机字符池为空;已安全跳过。");
|
|
return;
|
|
}
|
|
|
|
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)));
|
|
if (glitchController != null)
|
|
{
|
|
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);
|
|
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;
|
|
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?.SetTruthWarmVisuals(truthWarmColor);
|
|
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();
|
|
|
|
if (glitchController != null)
|
|
glitchController.Intensity = 0f;
|
|
memoryMotionWeight = 0f;
|
|
state = PresentationState.TruthHolding;
|
|
}
|
|
|
|
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)));
|
|
}
|
|
|
|
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);
|
|
|
|
if (glitchController != null)
|
|
{
|
|
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
|
|
glitchController.TransitionTo(Mathf.Clamp01(glitchPeak), rise);
|
|
}
|
|
|
|
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.LieHolding,
|
|
PresentationState.LieCracking))
|
|
yield break;
|
|
|
|
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?.SetTruthWarmVisuals(truthWarmColor);
|
|
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();
|
|
if (glitchController != null)
|
|
glitchController.TransitionTo(0f, 0.18f);
|
|
memoryMotionWeight = 0f;
|
|
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();
|
|
if (glitchController != null)
|
|
{
|
|
glitchController.ClearCalmField(true);
|
|
glitchController.TransitionTo(0f, 0.12f);
|
|
glitchController.SetAmbientNoise(0f, 320f, 0f);
|
|
}
|
|
|
|
yield return new WaitForSeconds(memoryExitDuration);
|
|
DisableMemoryRenderer(memoryRendererA);
|
|
DisableMemoryRenderer(memoryRendererB);
|
|
|
|
if (currentMemoryKind == MemoryKind.PrivateOffice)
|
|
{
|
|
particleManager?.SetTargetParticlesAlpha(0.6f, 0.18f);
|
|
if (glitchController != null)
|
|
{
|
|
glitchController.SetAmbientNoise(0.012f, 430f, 0.24f);
|
|
glitchController.Intensity = 0.03f;
|
|
}
|
|
}
|
|
|
|
state = PresentationState.TruthHolding;
|
|
}
|
|
|
|
public void ForceCleanupImmediate(bool hideFace)
|
|
{
|
|
DOTween.Kill(this);
|
|
StopAllCoroutines();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (glitchController != null)
|
|
{
|
|
glitchController.Intensity = 0f;
|
|
glitchController.ClearCalmField(true);
|
|
glitchController.SetAmbientNoise(0f, 320f, 0f);
|
|
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.OpaqueBackground);
|
|
}
|
|
|
|
particleManager?.RestoreTargetPresentationDefaults();
|
|
state = PresentationState.Idle;
|
|
currentLieKeyword = string.Empty;
|
|
currentLeakFragment = string.Empty;
|
|
memoryMotionWeight = 0f;
|
|
memoryZoom = 1f;
|
|
horizontalTear = 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)
|
|
{
|
|
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);
|
|
memoryOverdrive = Mathf.Lerp(0.35f, 1f, eased);
|
|
memoryDamage = Mathf.Max(memoryDamage, Mathf.Lerp(0.66f, 1f, eased));
|
|
|
|
if (elapsed >= nextShuffle)
|
|
{
|
|
string value = BuildRandomActorText(pool, characterCount);
|
|
SetActorOverdriveText(value, eased);
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
SetTextActive(actorFlashText, value, Color.white);
|
|
SetTextActive(
|
|
actorFlashChromaRed,
|
|
value,
|
|
new Color(1f, 0.18f, 0.18f, Mathf.Lerp(0.22f, 0.38f, progress)));
|
|
SetTextActive(
|
|
actorFlashChromaCyan,
|
|
value,
|
|
new Color(0.16f, 0.92f, 1f, Mathf.Lerp(0.22f, 0.38f, progress)));
|
|
|
|
Vector3 center = actorFlashText.transform.position;
|
|
float chromaDistance = Mathf.Lerp(0.04f, 0.11f, progress);
|
|
float verticalJitter = UnityEngine.Random.Range(-0.012f, 0.012f) * 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();
|
|
}
|
|
|
|
private IEnumerator PulseGlitch(float peak, float riseDuration, float fallDuration)
|
|
{
|
|
if (glitchController == null)
|
|
yield break;
|
|
|
|
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
|
|
yield return glitchController.TransitionToAndWait(Mathf.Clamp01(peak), riseDuration);
|
|
yield return glitchController.TransitionToAndWait(0f, fallDuration);
|
|
}
|
|
|
|
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;
|
|
glitchController?.SetCalmField(truthBounds);
|
|
}
|
|
|
|
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.88f,
|
|
MemoryKind.Sunset => 0.94f,
|
|
_ => 0.76f
|
|
};
|
|
contrast = kind == MemoryKind.PrivateOffice ? 1.20f : 1.08f;
|
|
vignette = kind switch
|
|
{
|
|
MemoryKind.Office => 0.24f,
|
|
MemoryKind.Sunset => 0.18f,
|
|
_ => 0.46f
|
|
};
|
|
noise = 0.04f + preset.BaseDistortion * 0.28f;
|
|
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);
|
|
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>();
|
|
lieClipper.Initialize(lieText, expressionScreenMaskRect);
|
|
lieGhostClipper.Initialize(lieGhostText, expressionScreenMaskRect);
|
|
systemClipper.Initialize(systemText, expressionScreenMaskRect);
|
|
actorFlashClipper.Initialize(actorFlashText, expressionScreenMaskRect);
|
|
actorFlashChromaRedClipper.Initialize(actorFlashChromaRed, expressionScreenMaskRect);
|
|
actorFlashChromaCyanClipper.Initialize(actorFlashChromaCyan, 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;
|
|
|
|
runtimeObjectsReady = true;
|
|
}
|
|
|
|
private SpriteRenderer CreateMemoryRenderer(string objectName)
|
|
{
|
|
GameObject obj = new GameObject(objectName);
|
|
obj.transform.SetParent(runtimeRoot, false);
|
|
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.enableAutoSizing = 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);
|
|
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);
|
|
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();
|
|
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(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)
|
|
{
|
|
if (renderer == null)
|
|
return;
|
|
DOTween.To(
|
|
() => GetMemoryOpacity(renderer),
|
|
value => SetMemoryOpacity(renderer, value),
|
|
Mathf.Clamp01(target),
|
|
Mathf.Max(0.01f, duration))
|
|
.SetEase(Ease.OutQuad)
|
|
.SetTarget(this);
|
|
}
|
|
|
|
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;
|
|
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 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()
|
|
{
|
|
StopActorOverdriveImmediate();
|
|
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);
|
|
}
|
|
|
|
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 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);
|
|
}
|
|
}
|
|
}
|