diff --git a/Assets/Scenes/HuoShanFixScene.unity b/Assets/Scenes/HuoShanFixScene.unity
index 601d5da39..1df79dc17 100644
--- a/Assets/Scenes/HuoShanFixScene.unity
+++ b/Assets/Scenes/HuoShanFixScene.unity
@@ -9251,6 +9251,7 @@ MonoBehaviour:
_currentStage: 0
_intensity: 0
_transitionDuration: 0.5
+ _compositeMode: 0
_stage1Clip: {fileID: 8300000, guid: 9f4ca2e877b18ff46902c106f56ed672, type: 3}
_stage2Clip: {fileID: 8300000, guid: 5b5068defdc3579479bf0efa306a07ca, type: 3}
_stage3Clip: {fileID: 8300000, guid: a965c5c0ea798db4787e279d5bef59fa, type: 3}
@@ -13253,7 +13254,7 @@ GameObject:
- component: {fileID: 1711705855}
- component: {fileID: 1711705856}
m_Layer: 5
- m_Name: bound
+ m_Name: ExpressionScreenMaskRect
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -15006,6 +15007,7 @@ GameObject:
m_Component:
- component: {fileID: 2138889539}
- component: {fileID: 2138889540}
+ - component: {fileID: 2138889543}
- component: {fileID: 2138889542}
- component: {fileID: 2138889541}
m_Layer: 0
@@ -15048,6 +15050,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
particleManager: {fileID: 3669563163941125876}
expressionView: {fileID: 5162511683866016917}
+ logReleasePresentation: {fileID: 2138889543}
defaultAnxietyPhrases:
- "\u6211\u597D\u6015"
- "\u600E\u4E48\u529E"
@@ -15060,6 +15063,36 @@ MonoBehaviour:
ambientNoiseDensity: 0
ambientNoiseScale: 360
ambientNoiseBrightness: 0.5
+--- !u!114 &2138889543
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2138889538}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 2d202e7d79d64fb5a9d7d42e974cbacd, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ particleManager: {fileID: 3669563163941125876}
+ faceRenderer: {fileID: 2468058580162010626}
+ glitchController: {fileID: 741515695}
+ expressionScreenMaskRect: {fileID: 1711705855}
+ screenFont: {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2}
+ officeMemory: {fileID: 21300000, guid: 1f0146f4ad75ca54da8909a24be3ce03, type: 3}
+ sunsetMemory: {fileID: 21300000, guid: 4d9b651ad3ad15941a4a49bb381c5a2d, type: 3}
+ privateOfficeMemory: {fileID: 21300000, guid: 56e0c1ba37badb94d8fa982258ade9e7, type: 3}
+ memoryMaterial: {fileID: 2100000, guid: f99b647eb8e7425fb1f0e32590dfc77d, type: 2}
+ faceFadeDuration: 1
+ memoryFadeDuration: 0.55
+ memoryExitDuration: 0.45
+ faceDipRatio: 0.015
+ lieColor: {r: 0.66, g: 0.94, b: 1, a: 1}
+ errorColor: {r: 1, g: 0.34, b: 0.18, a: 1}
+ truthWarmColor: {r: 1, g: 0.84, b: 0.64, a: 1}
+ screenGlowSortingOrder: 8
+ screenTextSortingOrder: 18
--- !u!114 &2138889541
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -16677,7 +16710,7 @@ SpriteRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 2
- m_Sprite: {fileID: 21300000, guid: 3af0a72c847ead14baf8582c9358a985, type: 3}
+ m_Sprite: {fileID: 21300000, guid: 6d0210d3312a4bd4a5042d6aa3d59f5b, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_FlipX: 0
m_FlipY: 0
@@ -17295,6 +17328,7 @@ MonoBehaviour:
clampCompletionPhasePositions: 1
completionScreenClipRect: {fileID: 1711705855}
completionClipEdgePadding: 0.2
+ minimumScreenMargin: 0.12
completionPositionSmoothTime: 0.2
languageDeepPanel1: {fileID: 1752165850}
languageDeepPanel2: {fileID: 2141248582}
@@ -17304,6 +17338,9 @@ MonoBehaviour:
expressionConfirmTimelineName: "\u706B\u5C71\u8868\u8FBE\u6A21\u5757/\u706B\u5C71\u8868\u8FBE\u786E\u8BA4"
focusZoomScale: 1.6
focusMoveDuration: 1.2
+ focusFadeDuration: 0.6
+ logOperationFadeDuration: 0.6
+ glowSortingOrder: 8
focusFadeRatio: 0.8
restartExpressionFadeDuration: 0.55
probabilityDuration: 3.5
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs
index 3c371ca3e..d885df199 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs
@@ -215,6 +215,8 @@ namespace AibisDream.MiniGame.Language
///
protected override string GetNextDisplayChar()
{
+ if (TryGetOverridePoolChar(out string overrideChar))
+ return overrideChar;
return IsTarget ? GetRandomChar() : GetRandomCharFromPhrases();
}
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs
index 3027a639f..f2b1e929d 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs
@@ -37,6 +37,10 @@ namespace AibisDream.MiniGame.Language
private float fadeOutAlpha;
private float nonTargetAlphaScale = 1f;
private float targetAlphaScale = 1f;
+ private Bounds screenBounds;
+ private bool hasScreenBounds;
+ private bool screenClippingEnabled;
+ private bool warnedMissingBounds;
public float NonTargetAlphaScale
{
@@ -62,11 +66,30 @@ namespace AibisDream.MiniGame.Language
fadeOutAlpha = fadeAlpha;
}
+ public void SetScreenBounds(Bounds bounds, bool valid = true)
+ {
+ screenBounds = bounds;
+ hasScreenBounds = valid && bounds.size.x > 0.001f && bounds.size.y > 0.001f;
+ if (hasScreenBounds)
+ warnedMissingBounds = false;
+ }
+
+ public void SetScreenClippingEnabled(bool enabled)
+ {
+ screenClippingEnabled = enabled;
+ }
+
public override void DrawShapes(Camera cam)
{
if (candidateParticles == null || candidateParticles.Count == 0)
return;
+ if (screenClippingEnabled && !hasScreenBounds && !warnedMissingBounds)
+ {
+ warnedMissingBounds = true;
+ Debug.LogWarning("[ConnectionRenderer] 未提供有效的 ExpressionScreenMaskRect,连线将沿用未裁切行为。");
+ }
+
using (Draw.Command(cam))
{
Draw.BlendMode = ShapesBlendMode.Transparent;
@@ -117,7 +140,7 @@ namespace AibisDream.MiniGame.Language
Color lineColor = ApplyGlow(nonTargetConnectionColor, nonTargetGlowIntensity);
lineColor.a = alpha * nonTargetAlphaScale;
- Draw.Line(p1.transform.position, p2.transform.position, lineColor);
+ DrawClippedLine(p1.transform.position, p2.transform.position, lineColor);
}
}
}
@@ -150,7 +173,7 @@ namespace AibisDream.MiniGame.Language
Color lineColor = ApplyGlow(targetConnectionColor, targetGlowIntensity);
lineColor.a = alpha * targetAlphaScale;
- Draw.Line(redParticles[i].transform.position, redParticles[j].transform.position, lineColor);
+ DrawClippedLine(redParticles[i].transform.position, redParticles[j].transform.position, lineColor);
}
}
}
@@ -172,7 +195,7 @@ namespace AibisDream.MiniGame.Language
// 绘制连接线(应用发光)
Color lineColor = ApplyGlow(propagationColor, propagationGlowIntensity);
lineColor.a = 0.3f;
- Draw.Line(info.source.transform.position, info.particle.transform.position, lineColor);
+ DrawClippedLine(info.source.transform.position, info.particle.transform.position, lineColor);
// 绘制脉冲光点
if (info.pulseProgress < 1f)
@@ -188,11 +211,15 @@ namespace AibisDream.MiniGame.Language
// 脉冲光点使用更强的 HDR 发光
Color pulseColor = ApplyGlow(new Color(1f, 0.78f, 0.39f, pulseAlpha), propagationGlowIntensity * 1.5f);
- Draw.Ring(pulsePos, pulseSize, pulseSize * 0.5f, pulseColor);
-
- // 光晕
- pulseColor.a = pulseAlpha * 0.5f;
- Draw.Disc(pulsePos, pulseSize * 1.5f, pulseColor);
+ float visualRadius = pulseSize * 1.5f;
+ if (TryConstrainPoint(pulsePos, visualRadius, out Vector3 containedPulse))
+ {
+ Draw.Ring(containedPulse, pulseSize, pulseSize * 0.5f, pulseColor);
+
+ // 光晕
+ pulseColor.a = pulseAlpha * 0.5f;
+ Draw.Disc(containedPulse, visualRadius, pulseColor);
+ }
}
// 绘制箭头
@@ -208,7 +235,9 @@ namespace AibisDream.MiniGame.Language
float angle = Mathf.Atan2(direction.y, direction.x);
Color arrowColor = ApplyGlow(new Color(1f, 0.78f, 0.39f, 0.8f), propagationGlowIntensity);
- DrawArrow(arrowPos, angle, 0.1f, arrowColor);
+ const float arrowSize = 0.1f;
+ if (TryConstrainPoint(arrowPos, arrowSize, out Vector3 containedArrow))
+ DrawArrow(containedArrow, angle, arrowSize, arrowColor);
}
}
}
@@ -231,6 +260,94 @@ namespace AibisDream.MiniGame.Language
Draw.Triangle(tip, left, right, color);
}
+ private void DrawClippedLine(Vector3 from, Vector3 to, Color color)
+ {
+ if (screenClippingEnabled && hasScreenBounds)
+ {
+ if (!TryClipLineToBounds(from, to, screenBounds, out from, out to))
+ return;
+ }
+
+ Draw.Line(from, to, color);
+ }
+
+ private bool TryConstrainPoint(Vector3 point, float radius, out Vector3 constrained)
+ {
+ if (!screenClippingEnabled || !hasScreenBounds)
+ {
+ constrained = point;
+ return true;
+ }
+
+ float minX = screenBounds.min.x + radius;
+ float maxX = screenBounds.max.x - radius;
+ float minY = screenBounds.min.y + radius;
+ float maxY = screenBounds.max.y - radius;
+ if (minX > maxX || minY > maxY)
+ {
+ constrained = point;
+ return false;
+ }
+
+ constrained = new Vector3(
+ Mathf.Clamp(point.x, minX, maxX),
+ Mathf.Clamp(point.y, minY, maxY),
+ point.z);
+ return true;
+ }
+
+ ///
+ /// Liang–Barsky line clipping against a world-space axis-aligned rectangle.
+ /// Kept public for component tests and editor diagnostics.
+ ///
+ public static bool TryClipLineToBounds(
+ Vector3 from,
+ Vector3 to,
+ Bounds bounds,
+ out Vector3 clippedFrom,
+ out Vector3 clippedTo)
+ {
+ clippedFrom = from;
+ clippedTo = to;
+
+ float dx = to.x - from.x;
+ float dy = to.y - from.y;
+ float t0 = 0f;
+ float t1 = 1f;
+
+ if (!ClipTest(-dx, from.x - bounds.min.x, ref t0, ref t1) ||
+ !ClipTest(dx, bounds.max.x - from.x, ref t0, ref t1) ||
+ !ClipTest(-dy, from.y - bounds.min.y, ref t0, ref t1) ||
+ !ClipTest(dy, bounds.max.y - from.y, ref t0, ref t1))
+ {
+ return false;
+ }
+
+ clippedFrom = Vector3.LerpUnclamped(from, to, t0);
+ clippedTo = Vector3.LerpUnclamped(from, to, t1);
+ return true;
+ }
+
+ private static bool ClipTest(float p, float q, ref float t0, ref float t1)
+ {
+ if (Mathf.Approximately(p, 0f))
+ return q >= 0f;
+
+ float r = q / p;
+ if (p < 0f)
+ {
+ if (r > t1) return false;
+ if (r > t0) t0 = r;
+ }
+ else
+ {
+ if (r < t0) return false;
+ if (r < t1) t1 = r;
+ }
+
+ return true;
+ }
+
///
/// 设置连接线样式
///
@@ -279,4 +396,3 @@ namespace AibisDream.MiniGame.Language
}
}
}
-
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
index ee27d806d..1c07b283d 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
@@ -12,9 +12,10 @@ namespace AibisDream
///
public class ExpressionManager : MonoBehaviour
{
- [Header("系统组件")]
- [SerializeField] private LanguageParticleManager particleManager;
- [SerializeField] private GameObject expressionView;
+ [Header("系统组件")]
+ [SerializeField] private LanguageParticleManager particleManager;
+ [SerializeField] private GameObject expressionView;
+ [SerializeField] private LogReleasePresentationController logReleasePresentation;
[Header("默认配置")]
[SerializeField] private List defaultAnxietyPhrases = new List
@@ -32,6 +33,8 @@ namespace AibisDream
[Tooltip("噪波故障效果控制器,可选;置于 Expression View 子物体上")]
[SerializeField] private SpriteNoiseGlitchController glitchController;
+ public LogReleasePresentationController LogReleasePresentation => logReleasePresentation;
+
void Start()
{
// 注册到系统字典
@@ -48,16 +51,26 @@ namespace AibisDream
expressionView.SetActive(false);
}
- // 查找粒子管理器
- if (particleManager == null)
- {
- particleManager = GetComponentInChildren(true);
- }
+ // 查找粒子管理器
+ if (particleManager == null)
+ {
+ particleManager = GetComponentInChildren(true);
+ }
- if (glitchController == null)
- {
- glitchController = GetComponentInChildren(true);
- }
+ if (glitchController == null)
+ {
+ glitchController = GetComponentInChildren(true);
+ }
+
+ if (logReleasePresentation == null)
+ {
+ logReleasePresentation = GetComponent();
+ }
+
+ if (particleManager != null)
+ {
+ particleManager.SetPresentationController(logReleasePresentation);
+ }
}
///
@@ -89,6 +102,7 @@ namespace AibisDream
{
if (particleManager != null)
{
+ particleManager.SetPresentationController(logReleasePresentation);
// 使用传入的配置,如果没有则使用默认配置
var phrases = anxietyPhrases ?? defaultAnxietyPhrases;
var sentence = targetSentence ?? defaultTargetSentence;
@@ -126,6 +140,12 @@ namespace AibisDream
yield return StartCoroutine(particleManager.FadeOutBeforeNewExpressionRound(duration));
}
+ public IEnumerator PreparePresentationForNextRound()
+ {
+ if (logReleasePresentation == null) yield break;
+ yield return logReleasePresentation.PrepareForNextRound();
+ }
+
///
/// 等待表达流程就绪(开场表现 + 火山表达panel 播完)
///
@@ -161,6 +181,11 @@ namespace AibisDream
///
public void CloseView()
{
+ if (logReleasePresentation != null)
+ {
+ logReleasePresentation.ForceCleanupImmediate(true);
+ }
+
if (expressionView != null)
{
expressionView.SetActive(false);
@@ -177,6 +202,11 @@ namespace AibisDream
///
public void ResetSystem(bool playEntranceEffect = true)
{
+ if (logReleasePresentation != null)
+ {
+ logReleasePresentation.ForceCleanupImmediate(true);
+ }
+
if (particleManager != null)
{
particleManager.ResetGame(playEntranceEffect);
@@ -235,5 +265,66 @@ namespace AibisDream
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
+ public IEnumerator BeginExpressionMemory(
+ string memoryKey,
+ string preset,
+ float fadeDuration = -1f,
+ float pushSpeed = -1f,
+ float horizontalSpeed = -1f,
+ bool preserveGlitch = false)
+ {
+ if (logReleasePresentation == null)
+ {
+ Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;记忆命令安全结束。");
+ yield break;
+ }
+ yield return logReleasePresentation.BeginMemory(
+ memoryKey,
+ preset,
+ fadeDuration,
+ pushSpeed,
+ horizontalSpeed,
+ preserveGlitch);
+ }
+
+ public IEnumerator BeginExpressionLie(string keyword, string preset)
+ {
+ if (logReleasePresentation == null)
+ {
+ Debug.LogError("[ExpressionManager] LogReleasePresentationController 未配置;谎话命令安全结束。");
+ yield break;
+ }
+ yield return logReleasePresentation.BeginLie(keyword, preset);
+ }
+
+ public IEnumerator BreakExpressionLie()
+ {
+ if (logReleasePresentation == null) yield break;
+ yield return logReleasePresentation.BreakLie();
+ }
+
+ public IEnumerator RevealExpressionTruth()
+ {
+ if (logReleasePresentation == null) yield break;
+ yield return logReleasePresentation.RevealTruth();
+ }
+
+ public IEnumerator EndExpressionMemory()
+ {
+ if (logReleasePresentation == null) yield break;
+ yield return logReleasePresentation.EndMemory();
+ }
+
+ public void ImpactExpressionTruth()
+ {
+ logReleasePresentation?.TruthImpact();
+ }
+
+ private void OnDisable()
+ {
+ if (logReleasePresentation != null)
+ logReleasePresentation.ForceCleanupImmediate(true);
+ }
+
+ }
}
-}
\ No newline at end of file
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
index 2821a6b6a..d07ab0d4f 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
@@ -120,6 +120,8 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private RectTransform completionScreenClipRect;
[Tooltip("在裁剪区基础上再向内缩进的世界单位距离,避免放大后的字半个挂在边外")]
[SerializeField] private float completionClipEdgePadding = 0.2f;
+ [Tooltip("屏幕内容的最小安全边距;最终边距还会根据字形、光晕、圆环和晃动幅度动态增大。")]
+ [SerializeField, Min(0f)] private float minimumScreenMargin = 0.12f;
[Tooltip("将目标粒子拉回屏幕裁剪区时的平滑时间(越大越柔和,略增加拖尾感)")]
[SerializeField, Range(0.02f, 0.55f)] private float completionPositionSmoothTime = 0.2f;
@@ -136,6 +138,8 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private float focusZoomScale = 1.6f;
[SerializeField] private float focusMoveDuration = 1.2f;
[SerializeField] private float focusFadeDuration = 1f;
+ [SerializeField] private float logOperationFadeDuration = 0.6f;
+ [SerializeField] private int glowSortingOrder = 8;
[Tooltip("再次 start_expression 时,若上一轮粒子/UI 仍在,先淡出再重开;协程 duration≤0 时用此值(秒)")]
[SerializeField] private float restartExpressionFadeDuration = 0.55f;
[SerializeField] private float probabilityDuration = 3.5f;
@@ -212,6 +216,8 @@ namespace AibisDream.MiniGame.Language
private Coroutine expressionPanelRoutine;
private Coroutine debugTestRoutine;
private Coroutine confirmTimelineRoutine;
+ private Coroutine focusTransitionRoutine;
+ private Coroutine slotMachineRoutine;
private readonly Dictionary entranceTargets = new Dictionary();
private class TargetFocusVisual
@@ -219,18 +225,22 @@ namespace AibisDream.MiniGame.Language
public CandidateParticle particle;
public GameObject ringObject;
public Disc ringDisc;
- public TMP_Text probabilityLabel;
- public float probability = 90f;
- public float targetProbability = 100f;
public float startShakeIntensity;
public string finalChar;
public bool stabilized;
public Vector3 focusPosition;
public Vector3 finalPosition;
+ public Vector3 lieEdgePosition;
public int orderIndex;
}
private bool focusTweensCompleted = false;
+ private bool focusMoveTweensCompleted;
+ private float resolveTotalDurationOverride = -1f;
+ private float lieEdgeCompression;
+ private float lieEdgeJitter;
+ private LogReleasePresentationController presentationController;
+ private const string LieCompressionTweenId = "HuoshanLieEdgeCompression";
// 边界
private Bounds movementBounds;
@@ -350,6 +360,9 @@ namespace AibisDream.MiniGame.Language
if (!isInitialized)
yield break;
+ StopFocusTransitionRoutine();
+ KillCompletionUiTweens();
+ resolveTotalDurationOverride = -1f;
float d = duration > 0f ? duration : restartExpressionFadeDuration;
// 火山释放 log 与文字同时叠在屏幕上时,先把 log 藏起来,等文字淡出结束后再由 FadeInReleaseLogIfNeeded 出现
@@ -440,6 +453,7 @@ namespace AibisDream.MiniGame.Language
///
public void SetInitialStatesBeforeOpen()
{
+ RestoreCompletionUiAlpha();
if (languageDeepPanel1 != null)
languageDeepPanel1.SetActive(false);
if (languageDeepPanel2 != null)
@@ -471,6 +485,10 @@ namespace AibisDream.MiniGame.Language
// 如果未初始化,直接返回
if (!isInitialized)
return;
+
+ StopFocusTransitionRoutine();
+ KillCompletionUiTweens();
+ RestoreTargetPresentationDefaults();
if (entranceRoutine != null)
{
@@ -502,6 +520,7 @@ namespace AibisDream.MiniGame.Language
{
if (particle != null)
{
+ DOTween.Kill(particle);
particle.transform.DOKill();
}
}
@@ -509,6 +528,7 @@ namespace AibisDream.MiniGame.Language
{
if (particle != null)
{
+ DOTween.Kill(particle);
particle.transform.DOKill();
}
}
@@ -518,6 +538,7 @@ namespace AibisDream.MiniGame.Language
completionPhase = CompletionPhase.None;
allowInput = false;
ClearCompletionClampSmoothVelocity();
+ ReleaseScreenClipMaterials();
}
private void InitializeCanvas()
@@ -589,6 +610,7 @@ namespace AibisDream.MiniGame.Language
particle.SetColor(floatingTextColor);
particle.SetFontSize(floatingTextFontSize);
SetParticleBounds(particle);
+ ConfigureParticleScreenMask(particle);
// 设置发光效果
if (enableBloomGlow)
@@ -635,6 +657,7 @@ namespace AibisDream.MiniGame.Language
particle.SetFontStyles(targetParticleFontSize, nonTargetParticleFontSize, targetParticleBold, nonTargetParticleBold);
particle.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude);
SetParticleBounds(particle);
+ ConfigureParticleScreenMask(particle);
if (enableBloomGlow)
{
@@ -690,6 +713,16 @@ namespace AibisDream.MiniGame.Language
particle.SetCircularBounds(boundsCenter, boundsRadius);
}
+ private void ConfigureParticleScreenMask(TextParticle particle)
+ {
+ if (particle == null)
+ return;
+
+ particle.SetScreenClipRect(completionScreenClipRect);
+ int sortingLayerId = worldCanvas != null ? worldCanvas.sortingLayerID : 0;
+ particle.SetGlowMasking(sortingLayerId, glowSortingOrder);
+ }
+
private void InitializeRenderers()
{
// 连接线渲染器
@@ -899,6 +932,21 @@ namespace AibisDream.MiniGame.Language
/// 完成阶段使用的裁剪范围:优先用 Inspector 指定的屏幕 Rect,否则与粒子运动边界一致。
///
private Bounds GetCompletionClipBounds()
+ {
+ Bounds b = GetScreenWorldBounds();
+ Vector2 safetyPadding = CalculateScreenSafetyPadding();
+ if (safetyPadding.sqrMagnitude > 0f)
+ {
+ Bounds shrunk = b;
+ shrunk.Expand(new Vector3(-safetyPadding.x * 2f, -safetyPadding.y * 2f, 0f));
+ if (shrunk.size.x >= 0.01f && shrunk.size.y >= 0.01f)
+ b = shrunk;
+ }
+
+ return b;
+ }
+
+ public Bounds GetScreenWorldBounds()
{
Bounds b;
if (completionScreenClipRect != null)
@@ -924,17 +972,40 @@ namespace AibisDream.MiniGame.Language
b = movementBounds;
}
- if (completionClipEdgePadding > 0f)
+ return b;
+ }
+
+ private Vector2 CalculateScreenSafetyPadding()
+ {
+ float padding = Mathf.Max(minimumScreenMargin, completionClipEdgePadding);
+ float glyphExtent = 0f;
+ float glowExtent = 0f;
+
+ foreach (CandidateParticle particle in targetParticles)
{
- Bounds shrunk = b;
- shrunk.Expand(-completionClipEdgePadding);
- if (shrunk.size.x >= 0.01f && shrunk.size.y >= 0.01f)
+ if (particle == null)
+ continue;
+
+ if (particle.textMesh != null && particle.textMesh.renderer != null)
{
- b = shrunk;
+ Bounds textBounds = particle.textMesh.renderer.bounds;
+ glyphExtent = Mathf.Max(glyphExtent, textBounds.extents.x, textBounds.extents.y);
+ }
+
+ if (particle.glowSprite != null)
+ {
+ Bounds glowBounds = particle.glowSprite.bounds;
+ glowExtent = Mathf.Max(glowExtent, glowBounds.extents.x, glowBounds.extents.y);
}
}
- return b;
+ float zoomedGlyph = glyphExtent * Mathf.Max(1f, focusZoomScale);
+ float focusRingRadius = Mathf.Clamp(targetParticleFontSize * 0.05f, 0.35f, 0.75f) +
+ Mathf.Max(focusRingThickness, 0f);
+ float shakeExtent = Mathf.Max(shuffleDistance, nonTargetShakeIntensity, completionPositionSmoothTime * 0.1f);
+
+ float horizontal = Mathf.Max(padding, zoomedGlyph, glowExtent, focusRingRadius, shakeExtent);
+ return new Vector2(horizontal, horizontal);
}
private static Vector3 ClampPositionToBounds(Vector3 worldPos, Bounds b)
@@ -951,6 +1022,11 @@ namespace AibisDream.MiniGame.Language
// 更新连接线渲染器
if (connectionRenderer != null)
{
+ Bounds screenBounds = GetScreenWorldBounds();
+ connectionRenderer.SetScreenBounds(
+ screenBounds,
+ completionScreenClipRect != null || (screenBounds.size.x > 0.001f && screenBounds.size.y > 0.001f));
+ connectionRenderer.SetScreenClippingEnabled(ShouldClipConnections(completionPhase));
connectionRenderer.SetData(
candidateParticles,
s_emptyPropagations,
@@ -1225,6 +1301,7 @@ namespace AibisDream.MiniGame.Language
private void BeginFocusSequence()
{
+ StopFocusTransitionRoutine();
if (focusSequenceButton != null)
{
focusSequenceButton.interactable = false;
@@ -1235,17 +1312,21 @@ namespace AibisDream.MiniGame.Language
completionPhase = CompletionPhase.Focusing;
focusTimer = 0f;
focusTweensCompleted = false;
+ focusMoveTweensCompleted = false;
+ lieEdgeCompression = 0f;
+ lieEdgeJitter = 0f;
interferenceLogicSuspended = true;
SetStatusUIVisibility(false, statusFadeOutDuration);
// 两个 panel、confirm button、火山释放 log 界面在 focus 触发时淡出
- FadeOutUIElement(languageDeepPanel1, focusFadeDuration);
- FadeOutUIElement(languageDeepPanel2, focusFadeDuration);
+ float operationFadeDuration = Mathf.Max(0.01f, logOperationFadeDuration);
+ FadeOutUIElement(languageDeepPanel1, operationFadeDuration);
+ FadeOutUIElement(languageDeepPanel2, operationFadeDuration);
if (focusSequenceButton != null)
{
- FadeOutUIElement(focusSequenceButton.gameObject, focusFadeDuration);
+ FadeOutUIElement(focusSequenceButton.gameObject, operationFadeDuration);
}
- FadeOutSpriteRenderer(releaseLogSpriteRenderer, focusFadeDuration);
+ FadeOutSpriteRenderer(releaseLogSpriteRenderer, operationFadeDuration);
if (connectionRenderer != null)
{
@@ -1257,7 +1338,7 @@ namespace AibisDream.MiniGame.Language
{
particle.isCalmed = true;
particle.velocity = Vector2.zero;
- FadeOutTextParticle(particle, focusFadeDuration);
+ FadeOutTextParticle(particle, operationFadeDuration);
}
foreach (var particle in candidateParticles)
@@ -1266,7 +1347,7 @@ namespace AibisDream.MiniGame.Language
{
particle.isCalmed = true;
particle.velocity = Vector2.zero;
- FadeOutTextParticle(particle, focusFadeDuration);
+ FadeOutTextParticle(particle, operationFadeDuration);
}
else
{
@@ -1306,9 +1387,7 @@ namespace AibisDream.MiniGame.Language
{
completedTweens++;
if (completedTweens >= tweenTargetCount)
- {
- EnterFocusHolding();
- }
+ focusMoveTweensCompleted = true;
});
particle.transform.DOScale(Vector3.one * focusZoomScale, focusMoveDuration).SetEase(Ease.InOutQuad);
@@ -1321,9 +1400,37 @@ namespace AibisDream.MiniGame.Language
}
if (tweenTargetCount == 0)
+ focusMoveTweensCompleted = true;
+
+ focusTransitionRoutine = StartCoroutine(WaitForFocusPresentationEntry());
+ }
+
+ private IEnumerator WaitForFocusPresentationEntry()
+ {
+ while (releaseLogSpriteRenderer != null &&
+ releaseLogSpriteRenderer.gameObject.activeSelf &&
+ releaseLogSpriteRenderer.enabled &&
+ releaseLogSpriteRenderer.color.a > 0.001f)
{
- EnterFocusHolding();
+ yield return null;
}
+
+ while (!focusMoveTweensCompleted && completionPhase == CompletionPhase.Focusing)
+ yield return null;
+
+ if (completionPhase == CompletionPhase.Focusing && !focusTweensCompleted)
+ EnterFocusHolding();
+
+ focusTransitionRoutine = null;
+ }
+
+ private void StopFocusTransitionRoutine()
+ {
+ if (focusTransitionRoutine == null)
+ return;
+
+ StopCoroutine(focusTransitionRoutine);
+ focusTransitionRoutine = null;
}
private TargetFocusVisual CreateFocusVisual(CandidateParticle particle, string finalChar)
@@ -1355,24 +1462,6 @@ namespace AibisDream.MiniGame.Language
disc.Color = ringColor;
visual.ringDisc = disc;
- GameObject labelObj = new GameObject("ProbabilityLabel");
- labelObj.layer = renderLayer;
- labelObj.transform.SetParent(particle.transform, false);
- RectTransform rectTransform = labelObj.AddComponent();
- rectTransform.localPosition = new Vector3(0f, estimatedRadius + 0.55f, 0f);
- rectTransform.localScale = Vector3.one;
- rectTransform.sizeDelta = new Vector2(2f, 0.6f);
-
- var label = labelObj.AddComponent();
- if (chineseFontAsset != null)
- {
- label.font = chineseFontAsset;
- }
- label.fontSize = 2f;
- label.alignment = TextAlignmentOptions.Center;
- label.text = $"{Mathf.RoundToInt(visual.probability)}%";
- visual.probabilityLabel = label;
-
return visual;
}
@@ -1390,14 +1479,21 @@ namespace AibisDream.MiniGame.Language
private void UpdateFocusHolding()
{
focusTimer += Time.deltaTime;
+ float compression = Mathf.SmoothStep(0f, 1f, lieEdgeCompression);
foreach (var visual in focusVisuals.Values)
{
if (visual == null || visual.particle == null)
continue;
- Vector3 shuffleOffset = CalculateShuffleOffset(visual, 0f, focusTimer);
- visual.particle.transform.position = visual.focusPosition + shuffleOffset;
+ Vector3 compressedPosition = Vector3.Lerp(
+ visual.focusPosition,
+ visual.lieEdgePosition,
+ compression);
+ Vector3 shuffleOffset = CalculateShuffleOffset(visual, compression, focusTimer) *
+ Mathf.Lerp(1f, 0.18f, compression);
+ Vector3 edgeJitter = CalculateLieEdgeJitter(visual, compression);
+ visual.particle.transform.position = compressedPosition + shuffleOffset + edgeJitter;
}
}
@@ -1407,28 +1503,31 @@ namespace AibisDream.MiniGame.Language
return;
focusTimer += Time.deltaTime;
- float baseInterval = Mathf.Max(0.2f, probabilityDuration);
+ CalculateResolveTiming(
+ focusVisuals.Count,
+ resolveTotalDurationOverride,
+ Mathf.Max(0.2f, probabilityDuration),
+ Mathf.Max(0f, predictionSequenceGap),
+ out float baseInterval,
+ out float activeStagger);
float arrangementDuration = Mathf.Max(0.2f, probabilityDuration * 0.6f);
+ if (resolveTotalDurationOverride > 0f)
+ arrangementDuration = Mathf.Max(0.05f, baseInterval * 0.65f);
foreach (var visual in focusVisuals.Values)
{
if (visual == null || visual.particle == null)
continue;
- float localTime = focusTimer - visual.orderIndex * predictionSequenceGap;
+ float localTime = focusTimer - visual.orderIndex * activeStagger;
float probabilityProgress = Mathf.Clamp01(localTime / baseInterval);
float arrangementProgress = Mathf.Clamp01(localTime / arrangementDuration);
- float probability = Mathf.Lerp(visual.probability, visual.targetProbability, probabilityProgress);
- if (visual.probabilityLabel != null)
- {
- visual.probabilityLabel.text = $"{Mathf.RoundToInt(probability)}%";
- }
-
float interval = Mathf.Lerp(0.05f, 0.8f, probabilityProgress);
visual.particle.SetChangeInterval(interval);
visual.particle.changeSpeedMultiplier = Mathf.Lerp(6f, 1f, probabilityProgress);
visual.particle.anxietyShakeIntensity = Mathf.Lerp(visual.startShakeIntensity, 0f, probabilityProgress);
+ visual.particle.calmProgress = probabilityProgress;
Vector3 dynamicTarget = GetDynamicArrangementTarget(visual, arrangementProgress, localTime);
Vector3 targetPosition = Vector3.Lerp(visual.focusPosition, dynamicTarget, Mathf.SmoothStep(0f, 1f, arrangementProgress));
@@ -1442,10 +1541,6 @@ namespace AibisDream.MiniGame.Language
visual.particle.ResetChangeTimer(visual.particle.changeSpeedMultiplier);
visual.particle.isStatic = true;
visual.stabilized = true;
- if (visual.probabilityLabel != null)
- {
- visual.probabilityLabel.text = "100%";
- }
}
}
@@ -1457,18 +1552,45 @@ namespace AibisDream.MiniGame.Language
}
///
- /// 从 FocusHolding 进入稳定化阶段(概率递增、文字锁定、排列到最终位置)
+ /// 从 FocusHolding 进入稳定化阶段(文字锁定、排列到最终位置)
///
- public void ResolveFocusSequence()
+ public void ResolveFocusSequence(float totalDuration = -1f)
{
if (completionPhase != CompletionPhase.FocusHolding)
return;
+ resolveTotalDurationOverride = totalDuration > 0f ? Mathf.Max(0.05f, totalDuration) : -1f;
completionPhase = CompletionPhase.Focusing;
focusTweensCompleted = true;
focusTimer = 0f;
}
+ public static void CalculateResolveTiming(
+ int characterCount,
+ float totalDuration,
+ float defaultCharacterDuration,
+ float defaultStagger,
+ out float characterDuration,
+ out float stagger)
+ {
+ int count = Mathf.Max(1, characterCount);
+ if (totalDuration <= 0f)
+ {
+ characterDuration = Mathf.Max(0.05f, defaultCharacterDuration);
+ stagger = Mathf.Max(0f, defaultStagger);
+ return;
+ }
+
+ float total = Mathf.Max(0.05f, totalDuration);
+ float staggerBudget = count > 1 ? total * 0.35f : 0f;
+ stagger = count > 1 ? staggerBudget / (count - 1) : 0f;
+ characterDuration = Mathf.Max(0.05f, total - staggerBudget);
+
+ float computedEnd = characterDuration + stagger * (count - 1);
+ if (computedEnd > total)
+ characterDuration = Mathf.Max(0.01f, characterDuration - (computedEnd - total));
+ }
+
///
/// 等待聚焦流程全部完成(稳定化 → Finished)
///
@@ -1480,6 +1602,366 @@ namespace AibisDream.MiniGame.Language
}
}
+ public static bool ShouldClipConnections(CompletionPhase phase)
+ {
+ return phase == CompletionPhase.Focusing ||
+ phase == CompletionPhase.FocusHolding ||
+ phase == CompletionPhase.Finished;
+ }
+
+ public void SetPresentationController(LogReleasePresentationController controller)
+ {
+ presentationController = controller;
+ }
+
+ public void BeginLieEdgeCompression(float duration, float jitterAmount)
+ {
+ if (completionPhase != CompletionPhase.FocusHolding)
+ {
+ Debug.LogWarning(
+ $"[LanguageParticleManager] 谎话边缘挤压在 {completionPhase} 状态被调用;已安全跳过。");
+ return;
+ }
+
+ Bounds edgeBounds = GetCompletionClipBounds();
+ int visualCount = Mathf.Max(1, focusVisuals.Count);
+ int index = 0;
+ foreach (TargetFocusVisual visual in focusVisuals.Values.OrderBy(value => value.orderIndex))
+ {
+ if (visual?.particle == null)
+ continue;
+
+ visual.lieEdgePosition = CalculateScreenEdgePosition(
+ edgeBounds,
+ index,
+ visualCount,
+ 0.04f);
+ visual.particle.isStatic = false;
+ visual.particle.anxietyShakeIntensity = Mathf.Max(visual.particle.anxietyShakeIntensity, 5.5f);
+ visual.particle.SetChangeInterval(0.045f);
+ index++;
+ }
+
+ DOTween.Kill(LieCompressionTweenId);
+ DOTween.To(
+ () => lieEdgeCompression,
+ value => lieEdgeCompression = value,
+ 1f,
+ Mathf.Max(0.05f, duration))
+ .SetEase(Ease.InCubic)
+ .SetId(LieCompressionTweenId)
+ .SetTarget(this);
+ DOTween.To(
+ () => lieEdgeJitter,
+ value => lieEdgeJitter = value,
+ Mathf.Max(0f, jitterAmount),
+ Mathf.Max(0.04f, duration * 0.7f))
+ .SetEase(Ease.OutQuad)
+ .SetId(LieCompressionTweenId)
+ .SetTarget(this);
+ }
+
+ ///
+ /// 老虎机换字演出:聚焦字在 duration 内只从 charPool 里疯狂换字,到点后自动恢复默认字符池并继续抖动。
+ ///
+ public void BeginSlotMachineShuffle(string charPool, float duration, float interval = 0.05f)
+ {
+ if (completionPhase != CompletionPhase.FocusHolding)
+ {
+ Debug.LogWarning(
+ $"[LanguageParticleManager] 老虎机换字在 {completionPhase} 状态被调用;已安全跳过。");
+ return;
+ }
+
+ StopSlotMachineShuffle();
+ string pool = SanitizeSlotMachinePool(charPool);
+ if (string.IsNullOrEmpty(pool))
+ {
+ Debug.LogWarning("[LanguageParticleManager] 老虎机换字字符池为空;已安全跳过。");
+ return;
+ }
+
+ float changeInterval = Mathf.Max(0.02f, interval);
+ foreach (TargetFocusVisual visual in focusVisuals.Values)
+ {
+ if (visual?.particle == null)
+ continue;
+
+ visual.particle.SetOverrideCharPool(pool);
+ visual.particle.SetChangeInterval(changeInterval);
+ visual.particle.ResetChangeTimer(changeInterval);
+ visual.particle.isStatic = false;
+ visual.particle.anxietyShakeIntensity = Mathf.Max(visual.particle.anxietyShakeIntensity, 5f);
+ }
+
+ slotMachineRoutine = StartCoroutine(EndSlotMachineShuffleAfter(Mathf.Max(0.05f, duration)));
+ }
+
+ ///
+ /// 清洗老虎机字符池:去掉空格与 | 分隔符,空结果返回 null。
+ ///
+ public static string SanitizeSlotMachinePool(string charPool)
+ {
+ if (string.IsNullOrEmpty(charPool))
+ return null;
+ string cleaned = charPool.Replace(" ", string.Empty).Replace("|", string.Empty);
+ return cleaned.Length == 0 ? null : cleaned;
+ }
+
+ public void StopSlotMachineShuffle()
+ {
+ if (slotMachineRoutine != null)
+ {
+ StopCoroutine(slotMachineRoutine);
+ slotMachineRoutine = null;
+ }
+
+ foreach (CandidateParticle particle in targetParticles)
+ particle?.SetOverrideCharPool(null);
+ }
+
+ private IEnumerator EndSlotMachineShuffleAfter(float duration)
+ {
+ yield return new WaitForSeconds(duration);
+ slotMachineRoutine = null;
+ foreach (CandidateParticle particle in targetParticles)
+ particle?.SetOverrideCharPool(null);
+ }
+
+ public void PrepareTruthReleaseFromLie()
+ {
+ DOTween.Kill(LieCompressionTweenId);
+ foreach (TargetFocusVisual visual in focusVisuals.Values)
+ {
+ if (visual?.particle == null)
+ continue;
+
+ // The resolve sequence now starts from the squeezed perimeter position,
+ // making every real character visibly rush back through the broken lie.
+ visual.focusPosition = visual.particle.transform.position;
+ visual.lieEdgePosition = visual.focusPosition;
+ visual.startShakeIntensity = Mathf.Max(visual.startShakeIntensity, 6.5f);
+ visual.particle.anxietyShakeIntensity = visual.startShakeIntensity;
+ }
+
+ lieEdgeCompression = 0f;
+ lieEdgeJitter = 0f;
+ }
+
+ public Bounds GetFinalTruthWorldBounds()
+ {
+ if (finalTargetPositions.Count == 0)
+ return GetTargetWorldBounds();
+
+ Bounds result = new Bounds(finalTargetPositions[0], Vector3.zero);
+ for (int i = 1; i < finalTargetPositions.Count; i++)
+ result.Encapsulate(finalTargetPositions[i]);
+
+ float glyphPadding = Mathf.Max(0.3f, targetParticleFontSize * 0.09f * focusZoomScale);
+ result.Expand(new Vector3(glyphPadding * 2f, glyphPadding * 2f, 0f));
+ return result;
+ }
+
+ public static Vector3 CalculateScreenEdgePosition(
+ Bounds bounds,
+ int index,
+ int count,
+ float inset)
+ {
+ int safeCount = Mathf.Max(1, count);
+ float angle = -Mathf.PI * 0.5f +
+ Mathf.PI * 2f * ((index + 0.5f) / safeCount);
+ Vector2 direction = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
+ float availableX = Mathf.Max(0f, bounds.extents.x - Mathf.Max(0f, inset));
+ float availableY = Mathf.Max(0f, bounds.extents.y - Mathf.Max(0f, inset));
+ float distanceX = Mathf.Abs(direction.x) > 0.0001f
+ ? availableX / Mathf.Abs(direction.x)
+ : float.PositiveInfinity;
+ float distanceY = Mathf.Abs(direction.y) > 0.0001f
+ ? availableY / Mathf.Abs(direction.y)
+ : float.PositiveInfinity;
+ float distance = Mathf.Min(distanceX, distanceY);
+ return bounds.center + (Vector3)(direction * distance);
+ }
+
+ public void SetTargetParticlesAlpha(float targetAlpha, float duration = 0f)
+ {
+ float alphaValue = Mathf.Clamp01(targetAlpha);
+ foreach (CandidateParticle particle in targetParticles)
+ {
+ if (particle == null) continue;
+ DOTween.Kill(particle);
+ if (duration <= 0f)
+ {
+ particle.alpha = alphaValue;
+ }
+ else
+ {
+ DOTween.To(() => particle.alpha, value => particle.alpha = value, alphaValue, duration)
+ .SetEase(Ease.OutQuad)
+ .SetTarget(particle);
+ }
+ }
+ }
+
+ public void SetTruthWarmVisuals(Color warmColor)
+ {
+ foreach (CandidateParticle particle in targetParticles)
+ {
+ if (particle == null) continue;
+ particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, warmColor);
+ }
+ }
+
+ public IEnumerator FlashTruthFragment(string fragment, float duration = 0.22f)
+ {
+ if (string.IsNullOrEmpty(fragment) || orderedRedParticles.Count == 0)
+ yield break;
+
+ int startIndex = targetSentence.IndexOf(fragment, System.StringComparison.Ordinal);
+ if (startIndex < 0)
+ {
+ Debug.LogWarning($"[LanguageParticleManager] 真话泄漏片段“{fragment}”不在目标句“{targetSentence}”中。");
+ yield break;
+ }
+
+ var leaked = new List();
+ int endExclusive = Mathf.Min(startIndex + fragment.Length, orderedRedParticles.Count);
+ for (int i = startIndex; i < endExclusive; i++)
+ {
+ CandidateParticle particle = orderedRedParticles[i];
+ if (particle == null) continue;
+ particle.ForceSetCharacter(targetSentence[i].ToString());
+ particle.isStatic = true;
+ particle.alpha = 1f;
+ leaked.Add(particle);
+ }
+
+ yield return new WaitForSeconds(Mathf.Max(0.02f, duration));
+
+ if (completionPhase == CompletionPhase.FocusHolding)
+ {
+ foreach (CandidateParticle particle in leaked)
+ {
+ if (particle == null) continue;
+ particle.alpha = 0.2f;
+ particle.isStatic = false;
+ particle.ResetChangeTimer(0.02f);
+ }
+ }
+ }
+
+ public Bounds GetTargetWorldBounds()
+ {
+ bool hasBounds = false;
+ Bounds result = new Bounds(
+ worldCanvas != null ? worldCanvas.transform.position : transform.position,
+ Vector3.zero);
+
+ foreach (CandidateParticle particle in targetParticles)
+ {
+ if (particle == null || particle.textMesh == null || particle.textMesh.renderer == null)
+ continue;
+
+ Bounds textBounds = particle.textMesh.renderer.bounds;
+ if (!hasBounds)
+ {
+ result = textBounds;
+ hasBounds = true;
+ }
+ else
+ {
+ result.Encapsulate(textBounds);
+ }
+ }
+
+ return result;
+ }
+
+ public IEnumerator ResolveFocusAndWait(float totalDuration)
+ {
+ if (completionPhase != CompletionPhase.FocusHolding)
+ {
+ Debug.LogWarning(
+ $"[LanguageParticleManager] ResolveFocusAndWait 在 {completionPhase} 状态被调用;已安全跳过。");
+ yield break;
+ }
+ ResolveFocusSequence(totalDuration);
+ yield return WaitUntilFocusFinished();
+ }
+
+ public void SnapTruthImmediate()
+ {
+ if (completionPhase != CompletionPhase.FocusHolding)
+ {
+ Debug.LogWarning(
+ $"[LanguageParticleManager] 真话瞬间稳定在 {completionPhase} 状态被调用;已安全跳过。");
+ return;
+ }
+
+ DOTween.Kill(LieCompressionTweenId);
+ StopSlotMachineShuffle();
+ completionPhase = CompletionPhase.Focusing;
+ focusTweensCompleted = true;
+ focusTimer = 0f;
+
+ foreach (TargetFocusVisual visual in focusVisuals.Values)
+ {
+ if (visual?.particle == null)
+ continue;
+
+ CandidateParticle particle = visual.particle;
+ particle.transform.DOKill();
+ particle.transform.position = visual.finalPosition;
+ particle.ForceSetCharacter(visual.finalChar);
+ particle.SetChangeInterval(1.5f);
+ particle.ResetChangeTimer(1.5f);
+ particle.anxietyShakeIntensity = 0f;
+ particle.changeSpeedMultiplier = 1f;
+ particle.calmProgress = 1f;
+ particle.isStatic = true;
+ visual.stabilized = true;
+ }
+
+ resolveTotalDurationOverride = -1f;
+ lieEdgeCompression = 0f;
+ lieEdgeJitter = 0f;
+ CompleteFocusSequence();
+ }
+
+ public void RestoreTargetPresentationDefaults(float alphaValue = 1f)
+ {
+ DOTween.Kill(LieCompressionTweenId);
+ StopSlotMachineShuffle();
+ foreach (CandidateParticle particle in targetParticles)
+ {
+ if (particle == null) continue;
+ DOTween.Kill(particle);
+ particle.alpha = Mathf.Clamp01(alphaValue);
+ particle.calmProgress = 0f;
+ particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor);
+ }
+ resolveTotalDurationOverride = -1f;
+ lieEdgeCompression = 0f;
+ lieEdgeJitter = 0f;
+ }
+
+ public void ReleaseScreenClipMaterials()
+ {
+ foreach (CandidateParticle particle in candidateParticles)
+ particle?.ClearScreenClip();
+ foreach (FloatingTextParticle particle in floatingParticles)
+ particle?.ClearScreenClip();
+ }
+
+ private void RebuildScreenClipMaterials()
+ {
+ foreach (CandidateParticle particle in candidateParticles)
+ ConfigureParticleScreenMask(particle);
+ foreach (FloatingTextParticle particle in floatingParticles)
+ ConfigureParticleScreenMask(particle);
+ }
+
private void DestroyFocusVisualDecorationsAndClear()
{
foreach (var visual in focusVisuals.Values)
@@ -1488,8 +1970,6 @@ namespace AibisDream.MiniGame.Language
continue;
if (visual.ringObject != null)
Destroy(visual.ringObject);
- if (visual.probabilityLabel != null)
- Destroy(visual.probabilityLabel.gameObject);
}
focusVisuals.Clear();
}
@@ -1515,6 +1995,8 @@ namespace AibisDream.MiniGame.Language
connectionRenderer.TargetAlphaScale = 0f;
}
+ lieEdgeCompression = 0f;
+ lieEdgeJitter = 0f;
completionPhase = CompletionPhase.Finished;
}
@@ -1558,16 +2040,30 @@ namespace AibisDream.MiniGame.Language
return new Vector3(x, y, 0f);
}
+ private Vector3 CalculateLieEdgeJitter(TargetFocusVisual visual, float compression)
+ {
+ if (visual == null || compression <= 0f || lieEdgeJitter <= 0f)
+ return Vector3.zero;
+
+ float seed = visual.orderIndex * 1.731f;
+ float x = Mathf.Sin(focusTimer * 29f + seed) +
+ Mathf.Sin(focusTimer * 53f + seed * 0.61f) * 0.35f;
+ float y = Mathf.Cos(focusTimer * 37f + seed * 1.17f) +
+ Mathf.Sin(focusTimer * 61f + seed) * 0.28f;
+ return new Vector3(x, y, 0f) * lieEdgeJitter * compression;
+ }
+
private void FadeOutTextParticle(TextParticle particle, float duration)
{
if (particle == null) return;
DOTween.To(() => particle.alpha, value => particle.alpha = value, 0f, duration)
- .SetEase(Ease.OutQuad);
+ .SetEase(Ease.OutQuad)
+ .SetTarget(particle);
if (particle.glowSprite != null)
{
- particle.glowSprite.DOFade(0f, duration);
+ particle.glowSprite.DOFade(0f, duration).SetTarget(particle);
}
}
@@ -1581,6 +2077,7 @@ namespace AibisDream.MiniGame.Language
var cg = obj.GetComponent();
if (cg == null)
cg = obj.AddComponent();
+ cg.DOKill();
cg.alpha = 1f;
cg.DOFade(0f, duration)
.SetEase(Ease.OutQuad)
@@ -1651,6 +2148,40 @@ namespace AibisDream.MiniGame.Language
sr.color = c;
}
+ private void KillCompletionUiTweens()
+ {
+ KillCanvasGroupTween(languageDeepPanel1);
+ KillCanvasGroupTween(languageDeepPanel2);
+ if (focusSequenceButton != null)
+ KillCanvasGroupTween(focusSequenceButton.gameObject);
+ if (releaseLogSpriteRenderer != null)
+ DOTween.Kill(releaseLogSpriteRenderer);
+ }
+
+ private static void KillCanvasGroupTween(GameObject obj)
+ {
+ if (obj == null) return;
+ CanvasGroup group = obj.GetComponent();
+ if (group != null)
+ group.DOKill();
+ }
+
+ private void RestoreCompletionUiAlpha()
+ {
+ RestoreCanvasGroupAlpha(languageDeepPanel1);
+ RestoreCanvasGroupAlpha(languageDeepPanel2);
+ if (focusSequenceButton != null)
+ RestoreCanvasGroupAlpha(focusSequenceButton.gameObject);
+ }
+
+ private static void RestoreCanvasGroupAlpha(GameObject obj)
+ {
+ if (obj == null) return;
+ CanvasGroup group = obj.GetComponent();
+ if (group != null)
+ group.alpha = 1f;
+ }
+
private Vector3 ComputeCentroid(List particles)
{
if (particles == null || particles.Count == 0)
@@ -1722,6 +2253,10 @@ namespace AibisDream.MiniGame.Language
public void ResetGame(bool playEntranceEffect = true)
{
+ StopFocusTransitionRoutine();
+ KillCompletionUiTweens();
+ RestoreTargetPresentationDefaults();
+ RestoreCompletionUiAlpha();
isCompleted = false;
completionPhase = CompletionPhase.None;
completionTimer = 0f;
@@ -1792,8 +2327,10 @@ namespace AibisDream.MiniGame.Language
p.velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
p.transform.position = GetRandomPositionInBounds();
p.isCalmed = false;
+ p.calmProgress = 0f;
p.alpha = p.baseAlpha;
p.transform.localScale = Vector3.one;
+ DOTween.Kill(p);
p.transform.DOKill();
p.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude);
@@ -1809,9 +2346,13 @@ namespace AibisDream.MiniGame.Language
{
floating.isCalmed = false;
floating.alpha = floating.baseAlpha;
+ DOTween.Kill(floating);
floating.transform.DOKill();
}
+ ReleaseScreenClipMaterials();
+ RebuildScreenClipMaterials();
+
SelectRedParticles();
UpdateStatusUI();
if (playEntranceEffect)
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
index 0b5ed66a9..69ddc6bc2 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
@@ -13,6 +13,19 @@ namespace AibisDream.MiniGame.Language
{
private static AibisDream.ExpressionManager ExpressionManager => FixSystemCenter.SystemDic.Get();
+ private static bool TryGetPresentation(out LogReleasePresentationController presentation)
+ {
+ presentation = ExpressionManager != null
+ ? ExpressionManager.LogReleasePresentation
+ : null;
+ if (presentation != null)
+ return true;
+
+ UnityEngine.Debug.LogError(
+ "[LanguageYarnCommand] LogReleasePresentationController 未找到;演出命令安全结束。");
+ return false;
+ }
+
///
/// 启动表达粒子系统(在打开视图后调用),等待火山表达panel播完再返回。
/// 流程:若有上一轮则先淡出字与连线 → 再淡入火山释放 log → 再启动新一轮并等表达 panel 就绪。
@@ -57,7 +70,8 @@ namespace AibisDream.MiniGame.Language
}
}
- // 先淡出上一轮文字/连线等,再淡入火山释放 log,最后开新一轮粒子(避免 log 与旧字叠在一起)
+ // 先清理上一轮演出,再淡出旧文字/连线并恢复 Log 操作界面。
+ yield return ExpressionManager.PreparePresentationForNextRound();
yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f);
yield return ExpressionManager.EnsureReleaseLogFadedIn(1f);
ExpressionManager.StartSystem(phrases, targetSentence, completionNode, nonTargetParticleTotal);
@@ -165,6 +179,400 @@ namespace AibisDream.MiniGame.Language
yield return ExpressionManager.StartCoroutine(ExpressionManager.TransitionGlitchAndWait(to, duration));
}
+ [YarnCommand("expression_memory_begin")]
+ public static IEnumerator ExpressionMemoryBegin(
+ string memoryKey,
+ string preset,
+ float fadeDuration = -1f,
+ float pushSpeed = -1f,
+ float horizontalSpeed = -1f,
+ bool preserveGlitch = false)
+ {
+ if (ExpressionManager == null)
+ {
+ UnityEngine.Debug.LogError("ExpressionManager 未找到!");
+ yield break;
+ }
+ yield return ExpressionManager.StartCoroutine(
+ ExpressionManager.BeginExpressionMemory(
+ memoryKey,
+ preset,
+ fadeDuration,
+ pushSpeed,
+ horizontalSpeed,
+ preserveGlitch));
+ }
+
+ ///
+ /// 显式淡入火山脸,并等待淡入完成。
+ /// <>
+ ///
+ [YarnCommand("expression_face_fade_in")]
+ public static IEnumerator ExpressionFaceFadeIn(float duration = 0.45f)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.BeginFaceEntry(duration));
+ }
+
+ [YarnCommand("expression_lie_begin")]
+ public static IEnumerator ExpressionLieBegin(string keyword, string preset)
+ {
+ if (ExpressionManager == null)
+ {
+ UnityEngine.Debug.LogError("ExpressionManager 未找到!");
+ yield break;
+ }
+ yield return ExpressionManager.StartCoroutine(
+ ExpressionManager.BeginExpressionLie(keyword, preset));
+ }
+
+ [YarnCommand("expression_lie_break")]
+ public static IEnumerator ExpressionLieBreak()
+ {
+ if (ExpressionManager == null)
+ {
+ UnityEngine.Debug.LogError("ExpressionManager 未找到!");
+ yield break;
+ }
+ yield return ExpressionManager.StartCoroutine(ExpressionManager.BreakExpressionLie());
+ }
+
+ [YarnCommand("expression_truth_reveal")]
+ public static IEnumerator ExpressionTruthReveal()
+ {
+ if (ExpressionManager == null)
+ {
+ UnityEngine.Debug.LogError("ExpressionManager 未找到!");
+ yield break;
+ }
+ yield return ExpressionManager.StartCoroutine(ExpressionManager.RevealExpressionTruth());
+ }
+
+ // 以下命令是一动作一命令。非 IEnumerator 命令只启动效果,节拍由 Yarn 的 wait 控制。
+
+ [YarnCommand("expression_lie_prepare")]
+ public static void ExpressionLiePrepare(float freezeDuration, float noiseTarget)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PrepareLieVisuals(freezeDuration, noiseTarget);
+ }
+
+ [YarnCommand("expression_truth_squeeze")]
+ public static void ExpressionTruthSqueeze(float duration, float jitter, float alpha)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.SqueezeTruthCharacters(duration, jitter, alpha);
+ }
+
+ [YarnCommand("expression_lie_show")]
+ public static void ExpressionLieShow(string keyword, float duration, float characterSpacing)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.ShowLieKeyword(keyword, duration, characterSpacing);
+ }
+
+ [YarnCommand("expression_scanline")]
+ public static void ExpressionScanline(float duration, bool stalled)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayScreenScanline(duration, stalled);
+ }
+
+ [YarnCommand("expression_system_message")]
+ public static void ExpressionSystemMessage(string value, string style, float fadeDuration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.ShowSystemMessage(value, style, fadeDuration);
+ }
+
+ [YarnCommand("expression_system_message_hide")]
+ public static void ExpressionSystemMessageHide(float fadeDuration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.HideSystemMessage(fadeDuration);
+ }
+
+ [YarnCommand("expression_lie_crack_begin")]
+ public static void ExpressionLieCrackBegin(float lieAlpha)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.BeginLieCracking(lieAlpha);
+ }
+
+ [YarnCommand("expression_screen_dim")]
+ public static void ExpressionScreenDim(float alpha, float riseDuration, float fallDuration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayScreenDim(alpha, riseDuration, fallDuration);
+ }
+
+ [YarnCommand("expression_memory_jolt")]
+ public static void ExpressionMemoryJolt(float offset, float duration, float tear)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayMemoryJolt(offset, duration, tear);
+ }
+
+ [YarnCommand("expression_truth_leak")]
+ public static IEnumerator ExpressionTruthLeak(string fragment, float duration)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.LeakTruthFragment(fragment, duration));
+ }
+
+ [YarnCommand("expression_lie_shuffle")]
+ public static void ExpressionLieShuffle(
+ string characterPool,
+ float duration,
+ float interval)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.StartLieCharacterShuffle(characterPool, duration, interval);
+ }
+
+ ///
+ /// 演员大字闪现(阻塞):抖动的粒子字瞬间隐藏,屏幕中央出现白色大字(红青色散双影+急速缩放),
+ /// 停顿 holdDuration 秒后恢复粒子字抖动。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_flash")]
+ public static IEnumerator ExpressionActorFlash(
+ string text,
+ float holdDuration = 0.6f,
+ float fontSize = 10.5f,
+ float settledScale = -1f,
+ float punchScale = -1f,
+ float chromaDistance = -1f,
+ float chromaAlpha = -1f,
+ float impact = -1f,
+ float glitchPeak = -1f)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.FlashActorLie(
+ text,
+ holdDuration,
+ fontSize,
+ settledScale,
+ punchScale,
+ chromaDistance,
+ chromaAlpha,
+ impact,
+ glitchPeak));
+ }
+
+ ///
+ /// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_slot")]
+ public static void ExpressionActorSlot(
+ string charPool,
+ float duration,
+ float interval = 0.05f,
+ float fontSize = 14f)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.StartActorSlotMachine(charPool, duration, interval, fontSize);
+ }
+
+ ///
+ /// 将 Glitch、记忆冲击与画面推近缓慢堆到峰值,停住后保持峰值,等待后续硬切。
+ /// <>
+ ///
+ [YarnCommand("expression_overload_peak")]
+ public static IEnumerator ExpressionOverloadPeak(
+ float riseDuration = 0.75f,
+ float holdDuration = 0.22f,
+ float zoom = 1.10f,
+ float glitchPeak = 1f,
+ float impact = 1f)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.BuildActorOverloadPeak(
+ riseDuration,
+ holdDuration,
+ zoom,
+ glitchPeak,
+ impact));
+ }
+
+ ///
+ /// 单字/短词满屏重击,并等待该拍结束。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_word_hit")]
+ public static IEnumerator ExpressionActorWordHit(
+ string text,
+ float holdDuration = 0.16f,
+ float fontSize = 18f,
+ float punchScale = 1.55f)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.PlayActorWordHit(text, holdDuration, fontSize, punchScale));
+ }
+
+ ///
+ /// 单列词组严格裁在表情屏内,像字幕一样无缝向下滚动。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_scroll")]
+ public static IEnumerator ExpressionActorScroll(
+ string phraseList,
+ float scrollDuration = 1.8f,
+ float loopDuration = 0.85f,
+ float holdDuration = 0.45f,
+ float fontSize = 5.2f)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.ScrollActorLies(
+ phraseList,
+ scrollDuration,
+ loopDuration,
+ holdDuration,
+ fontSize));
+ }
+
+ [YarnCommand("expression_memory_cut")]
+ public static void ExpressionMemoryCut()
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.CutMemoryAndGlitchImmediate();
+ }
+
+ [YarnCommand("expression_truth_snap")]
+ public static void ExpressionTruthSnap(float peripheral, float calmRadius)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.SnapTruthImmediate(peripheral, calmRadius);
+ }
+
+ [YarnCommand("expression_glitch_pulse")]
+ public static void ExpressionGlitchPulse(float peak, float riseDuration, float fallDuration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayGlitchPulse(peak, riseDuration, fallDuration);
+ }
+
+ [YarnCommand("expression_truth_blackout")]
+ public static IEnumerator ExpressionTruthBlackout(float duration, float alpha)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.HoldTruthBlackout(duration, alpha));
+ }
+
+ [YarnCommand("expression_lie_split")]
+ public static void ExpressionLieSplit(
+ float duration,
+ float distanceRatio,
+ float stretchX,
+ float squashY)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ {
+ presentation.SplitLieVisual(
+ duration,
+ distanceRatio,
+ stretchX,
+ squashY);
+ }
+ }
+
+ [YarnCommand("expression_truth_flash")]
+ public static void ExpressionTruthFlash(
+ float alpha,
+ float riseDuration,
+ float holdDuration,
+ float fallDuration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ {
+ presentation.PlayTruthFlash(
+ alpha,
+ riseDuration,
+ holdDuration,
+ fallDuration);
+ }
+ }
+
+ [YarnCommand("expression_memory_brightness_pulse")]
+ public static void ExpressionMemoryBrightnessPulse(
+ float amount,
+ float riseDuration,
+ float fallDuration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayMemoryBrightnessPulse(amount, riseDuration, fallDuration);
+ }
+
+ [YarnCommand("expression_memory_tear")]
+ public static void ExpressionMemoryTear(float peak, float duration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayMemoryTear(peak, duration);
+ }
+
+ [YarnCommand("expression_face_dip")]
+ public static void ExpressionFaceDip(float duration, float ratio)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayFaceDip(duration, ratio);
+ }
+
+ [YarnCommand("expression_truth_release")]
+ public static void ExpressionTruthRelease(float alphaDuration)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.ReleaseTruthCharacters(alphaDuration);
+ }
+
+ [YarnCommand("expression_truth_resolve")]
+ public static IEnumerator ExpressionTruthResolve(
+ float duration,
+ float peripheral,
+ float calmRadius)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.ResolveTruthCharacters(duration, peripheral, calmRadius));
+ }
+
+ [YarnCommand("expression_memory_end")]
+ public static IEnumerator ExpressionMemoryEnd()
+ {
+ if (ExpressionManager == null)
+ {
+ UnityEngine.Debug.LogError("ExpressionManager 未找到!");
+ yield break;
+ }
+ yield return ExpressionManager.StartCoroutine(ExpressionManager.EndExpressionMemory());
+ }
+
+ [YarnCommand("expression_truth_impact")]
+ public static void ExpressionTruthImpact()
+ {
+ if (ExpressionManager == null)
+ {
+ UnityEngine.Debug.LogError("ExpressionManager 未找到!");
+ return;
+ }
+ ExpressionManager.ImpactExpressionTruth();
+ }
+
// ///
// /// Glitch 噪波过渡(指定起止值),等待完成后再继续下一步。
// ///
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs
new file mode 100644
index 000000000..b10909ace
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs
@@ -0,0 +1,2228 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using DG.Tweening;
+using TMPro;
+using UnityEngine;
+
+namespace AibisDream.MiniGame.Language
+{
+ ///
+ /// 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.
+ ///
+ 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 actorScrollTexts = new List();
+ private readonly List actorScrollClippers = new List();
+ private bool actorFlashActive;
+ private int actorFlashCount;
+ private Coroutine actorOverdriveRoutine;
+
+ private Texture2D runtimeWhiteTexture;
+ private Sprite runtimeWhiteSprite;
+ private MaterialPropertyBlock memoryBlockA;
+ private MaterialPropertyBlock memoryBlockB;
+ private float memoryOpacityA;
+ private float memoryOpacityB;
+ private float saturation = 1f;
+ private float brightness = 1f;
+ private float contrast = 1f;
+ private float vignette;
+ private float noise;
+ private float 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);
+ }
+ }
+
+ ///
+ /// 演员大字闪现:glitch 一拍的瞬间隐藏抖动的粒子字,屏幕中央出现白色大字。
+ /// 三次冲击逐级增强并累积记忆损伤;第三次大字不再退回粒子,而是留给老虎机过载态接管。
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 老虎机换字演出:聚焦字在 duration 内只从 charPool 里疯狂换字(非阻塞,节拍由 Yarn wait 控制)。
+ ///
+ 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[] raw = phraseList.Split('|');
+ var result = new List(raw.Length);
+ foreach (string item in raw)
+ {
+ string value = item.Trim();
+ if (!string.IsNullOrEmpty(value))
+ result.Add(value);
+ }
+ return result.ToArray();
+ }
+
+ private void EnsureActorScrollTextPool(int count)
+ {
+ while (actorScrollTexts.Count < count)
+ {
+ int index = actorScrollTexts.Count;
+ TextMeshPro text = CreateScreenText(
+ $"ActorScrollText_{index:00}",
+ 3.2f,
+ screenTextSortingOrder - index % 2);
+ TMPRectClipper clipper = text.gameObject.AddComponent();
+ clipper.Initialize(text, expressionScreenMaskRect);
+ actorScrollTexts.Add(text);
+ actorScrollClippers.Add(clipper);
+ }
+ }
+
+ private void HideActorScrollImmediate()
+ {
+ foreach (TextMeshPro text in actorScrollTexts)
+ HideTextImmediate(text);
+ }
+
+ private IEnumerator TruthImpactRoutine()
+ {
+ float original = brightness;
+ float peak = original + 0.18f;
+ yield return DOTween.To(() => brightness, value => brightness = value, peak, 0.08f)
+ .SetEase(Ease.OutQuad)
+ .SetTarget(this)
+ .WaitForCompletion();
+ yield return DOTween.To(() => brightness, value => brightness = value, original, 0.18f)
+ .SetEase(Ease.InQuad)
+ .SetTarget(this)
+ .WaitForCompletion();
+ }
+
+ 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(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();
+ lieGhostClipper = lieGhostText.gameObject.AddComponent();
+ systemClipper = systemText.gameObject.AddComponent();
+ actorFlashClipper = actorFlashText.gameObject.AddComponent();
+ actorFlashChromaRedClipper = actorFlashChromaRed.gameObject.AddComponent();
+ actorFlashChromaCyanClipper = actorFlashChromaCyan.gameObject.AddComponent();
+ 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();
+ scanlineRenderer.sprite = runtimeWhiteSprite;
+ scanlineRenderer.color = new Color(0.55f, 0.95f, 1f, 0f);
+ scanlineRenderer.sortingLayerID = GetScreenSortingLayerId();
+ scanlineRenderer.sortingOrder = screenTextSortingOrder - 2;
+ scanlineRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
+ scanlineRenderer.enabled = false;
+
+ GameObject dimObject = new GameObject("ScreenBrightnessDip");
+ dimObject.transform.SetParent(runtimeRoot, false);
+ screenDimRenderer = dimObject.AddComponent();
+ screenDimRenderer.sprite = runtimeWhiteSprite;
+ screenDimRenderer.color = new Color(0.02f, 0.05f, 0.08f, 0f);
+ screenDimRenderer.sortingLayerID = GetScreenSortingLayerId();
+ screenDimRenderer.sortingOrder = screenGlowSortingOrder + 1;
+ screenDimRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
+ Bounds screen = GetScreenBounds();
+ screenDimRenderer.transform.position = screen.center;
+ screenDimRenderer.transform.localScale = new Vector3(screen.size.x, screen.size.y, 1f);
+ screenDimRenderer.enabled = false;
+
+ runtimeObjectsReady = true;
+ }
+
+ private SpriteRenderer CreateMemoryRenderer(string objectName)
+ {
+ GameObject obj = new GameObject(objectName);
+ obj.transform.SetParent(runtimeRoot, false);
+ SpriteRenderer renderer = obj.AddComponent();
+ renderer.sharedMaterial = memoryMaterial;
+ renderer.sortingOrder = 0;
+ renderer.maskInteraction = SpriteMaskInteraction.None;
+ renderer.enabled = false;
+ obj.SetActive(false);
+ return renderer;
+ }
+
+ private TextMeshPro CreateScreenText(string objectName, float fontSize, int sortingOrder)
+ {
+ GameObject obj = new GameObject(objectName);
+ obj.transform.SetParent(runtimeRoot, false);
+ RectTransform rect = obj.AddComponent();
+ TextMeshPro text = obj.AddComponent();
+ text.font = screenFont != null ? screenFont : TMP_Settings.defaultFontAsset;
+ text.fontSize = fontSize;
+ text.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();
+ 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(), 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);
+ }
+
+ ///
+ /// 记忆图作为全屏背景:按主相机视野 cover 缩放(铺满、超出部分裁掉),
+ /// 渲染顺序在脸后面(sortingOrder 0 vs 脸的 2),透过脸的透明区域露出。
+ ///
+ private void FitMemoryToView(SpriteRenderer renderer)
+ {
+ if (renderer == null || renderer.sprite == null)
+ return;
+
+ Vector2 viewportSize;
+ Vector3 viewportCenter;
+ Quaternion viewportRotation = Quaternion.identity;
+ Camera viewCamera = Camera.main;
+ if (viewCamera != null)
+ {
+ float planeZ = renderer.transform.position.z;
+ float height = viewCamera.orthographic
+ ? viewCamera.orthographicSize * 2f
+ : 2f * Mathf.Abs(planeZ - viewCamera.transform.position.z) *
+ Mathf.Tan(viewCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
+ viewportSize = new Vector2(height * viewCamera.aspect, height);
+ viewportCenter = new Vector3(
+ viewCamera.transform.position.x,
+ viewCamera.transform.position.y,
+ planeZ);
+ }
+ else if (faceRenderer != null && faceRenderer.sprite != null)
+ {
+ Vector3 faceScale = faceRenderer.transform.lossyScale;
+ Vector3 faceSpriteSize = faceRenderer.sprite.bounds.size;
+ viewportSize = new Vector2(
+ Mathf.Abs(faceSpriteSize.x * faceScale.x),
+ Mathf.Abs(faceSpriteSize.y * faceScale.y));
+ viewportCenter = faceRenderer.transform.position;
+ viewportRotation = faceRenderer.transform.rotation;
+ }
+ else
+ {
+ Bounds screen = GetScreenBounds();
+ float height = screen.size.y * 2.4f;
+ viewportSize = new Vector2(height * 16f / 9f, height);
+ viewportCenter = screen.center;
+ }
+
+ Vector2 spriteSize = renderer.sprite.bounds.size;
+ float uniformScale = Mathf.Max(
+ viewportSize.x / Mathf.Max(0.001f, spriteSize.x),
+ viewportSize.y / Mathf.Max(0.001f, spriteSize.y));
+ renderer.transform.SetPositionAndRotation(viewportCenter, viewportRotation);
+ renderer.transform.localScale = Vector3.one * uniformScale;
+ }
+
+ private void ApplyMemoryBlocks()
+ {
+ ApplyMemoryBlock(memoryRendererA, memoryBlockA, memoryOpacityA);
+ ApplyMemoryBlock(memoryRendererB, memoryBlockB, memoryOpacityB);
+ }
+
+ private void ApplyMemoryBlock(SpriteRenderer renderer, MaterialPropertyBlock block, float opacity)
+ {
+ if (renderer == null || block == null)
+ return;
+
+ renderer.GetPropertyBlock(block);
+ block.SetFloat(SaturationId, saturation);
+ block.SetFloat(BrightnessId, brightness);
+ block.SetFloat(ContrastId, contrast);
+ block.SetFloat(VignetteId, vignette);
+ block.SetFloat(NoiseId, noise);
+ block.SetFloat(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
protected virtual string GetNextDisplayChar()
{
+ if (TryGetOverridePoolChar(out string overrideChar))
+ return overrideChar;
return GetRandomChar();
}
+
+ ///
+ /// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。
+ ///
+ public void SetOverrideCharPool(string pool)
+ {
+ overrideCharPool = string.IsNullOrEmpty(pool) ? null : pool;
+ }
+
+ protected bool TryGetOverridePoolChar(out string character)
+ {
+ if (string.IsNullOrEmpty(overrideCharPool))
+ {
+ character = null;
+ return false;
+ }
+
+ character = overrideCharPool[Random.Range(0, overrideCharPool.Length)].ToString();
+ return true;
+ }
///
/// 设置全局干扰短句配置(笑话等,由 LanguageParticleManager 调用)
@@ -411,6 +467,26 @@ namespace AibisDream.MiniGame.Language
return Sprite.Create(texture, new Rect(0, 0, resolution, resolution), new Vector2(0.5f, 0.5f));
}
+
+ protected virtual void OnDestroy()
+ {
+ ClearScreenClip();
+
+ if (!ownsGlowSprite || glowSprite == null || glowSprite.sprite == null)
+ return;
+
+ Texture2D texture = glowSprite.sprite.texture;
+ if (Application.isPlaying)
+ {
+ Destroy(glowSprite.sprite);
+ if (texture != null) Destroy(texture);
+ }
+ else
+ {
+ DestroyImmediate(glowSprite.sprite);
+ if (texture != null) DestroyImmediate(texture);
+ }
+ }
}
}