diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
index 1c07b283d..bd97be840 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
@@ -267,11 +267,11 @@ namespace AibisDream
public IEnumerator BeginExpressionMemory(
string memoryKey,
- string preset,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
- bool preserveGlitch = false)
+ bool preserveGlitch = false,
+ float targetOpacity = 1f)
{
if (logReleasePresentation == null)
{
@@ -280,11 +280,11 @@ namespace AibisDream
}
yield return logReleasePresentation.BeginMemory(
memoryKey,
- preset,
fadeDuration,
pushSpeed,
horizontalSpeed,
- preserveGlitch);
+ preserveGlitch,
+ targetOpacity);
}
public IEnumerator BeginExpressionLie(string keyword, string preset)
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
index d07ab0d4f..06c101bb8 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
@@ -232,6 +232,8 @@ namespace AibisDream.MiniGame.Language
public Vector3 finalPosition;
public Vector3 lieEdgePosition;
public int orderIndex;
+ public bool gatherLocked;
+ public Vector3 scatterOffset;
}
private bool focusTweensCompleted = false;
@@ -239,8 +241,10 @@ namespace AibisDream.MiniGame.Language
private float resolveTotalDurationOverride = -1f;
private float lieEdgeCompression;
private float lieEdgeJitter;
+ private float truthGatherProgress;
private LogReleasePresentationController presentationController;
private const string LieCompressionTweenId = "HuoshanLieEdgeCompression";
+ private const string TruthGatherTweenId = "HuoshanTruthGather";
// 边界
private Bounds movementBounds;
@@ -1315,6 +1319,8 @@ namespace AibisDream.MiniGame.Language
focusMoveTweensCompleted = false;
lieEdgeCompression = 0f;
lieEdgeJitter = 0f;
+ DOTween.Kill(TruthGatherTweenId);
+ truthGatherProgress = 0f;
interferenceLogicSuspended = true;
SetStatusUIVisibility(false, statusFadeOutDuration);
@@ -1480,20 +1486,102 @@ namespace AibisDream.MiniGame.Language
{
focusTimer += Time.deltaTime;
float compression = Mathf.SmoothStep(0f, 1f, lieEdgeCompression);
+ float gather = Mathf.SmoothStep(0f, 1f, truthGatherProgress);
+ int visualCount = Mathf.Max(1, focusVisuals.Count);
foreach (var visual in focusVisuals.Values)
{
if (visual == null || visual.particle == null)
continue;
+ // 真话聚合尝试:悬浮位被拉向最终位置;谎话挤压仍拥有更高优先级
+ Vector3 gatherPosition = Vector3.Lerp(visual.focusPosition, visual.finalPosition, gather);
Vector3 compressedPosition = Vector3.Lerp(
- visual.focusPosition,
+ gatherPosition,
visual.lieEdgePosition,
compression);
Vector3 shuffleOffset = CalculateShuffleOffset(visual, compression, focusTimer) *
- Mathf.Lerp(1f, 0.18f, compression);
+ Mathf.Lerp(1f, 0.18f, compression) *
+ Mathf.Lerp(1f, 0.12f, gather);
Vector3 edgeJitter = CalculateLieEdgeJitter(visual, compression);
- visual.particle.transform.position = compressedPosition + shuffleOffset + edgeJitter;
+ visual.scatterOffset = Vector3.Lerp(visual.scatterOffset, Vector3.zero, Time.deltaTime * 5.5f);
+ visual.particle.transform.position =
+ compressedPosition + shuffleOffset + edgeJitter + visual.scatterOffset;
+
+ // 锁字判定用原始进度:target=0.92 时阈值 0.95 的最后一个字永远锁不住
+ UpdateTruthGatherLock(visual, truthGatherProgress, visualCount);
+ }
+ }
+
+ private void UpdateTruthGatherLock(TargetFocusVisual visual, float gather, int visualCount)
+ {
+ // 按字序阶梯式锁定:阈值 0.45~0.95。目标进度 <1 时最后几个字永远差一点锁不住。
+ float lockThreshold = 0.45f + 0.5f * (visual.orderIndex + 1f) / visualCount;
+ if (!visual.gatherLocked && gather >= lockThreshold)
+ {
+ visual.gatherLocked = true;
+ visual.particle.ForceSetCharacter(visual.finalChar);
+ visual.particle.SetChangeInterval(2f);
+ visual.particle.ResetChangeTimer(2f);
+ visual.particle.anxietyShakeIntensity = 0.6f;
+ visual.particle.isStatic = true;
+ }
+ else if (visual.gatherLocked && gather < lockThreshold - 0.08f)
+ {
+ visual.gatherLocked = false;
+ visual.particle.isStatic = false;
+ visual.particle.SetChangeInterval(0.06f);
+ visual.particle.ResetChangeTimer(0.06f);
+ visual.particle.anxietyShakeIntensity = Mathf.Max(visual.particle.anxietyShakeIntensity, 4.5f);
+ }
+ }
+
+ ///
+ /// 真话聚合尝试(非等待):FocusHolding 下把目标字拉向最终位置并按序逐字锁定。
+ /// target 小于 1 时最后几个字永远差一点锁不住,用于"感觉快要成功了"。
+ ///
+ public void BeginTruthGather(float target, float duration)
+ {
+ if (completionPhase != CompletionPhase.FocusHolding)
+ {
+ Debug.LogWarning(
+ $"[LanguageParticleManager] 真话聚合在 {completionPhase} 状态被调用;已安全跳过。");
+ return;
+ }
+
+ DOTween.Kill(TruthGatherTweenId);
+ DOTween.To(
+ () => truthGatherProgress,
+ value => truthGatherProgress = value,
+ Mathf.Clamp01(target),
+ Mathf.Max(0.05f, duration))
+ .SetEase(Ease.InOutSine)
+ .SetId(TruthGatherTweenId)
+ .SetTarget(this);
+ }
+
+ /// 真话聚合被打断:目标字向外爆散并恢复乱跳(非等待)。
+ public void ScatterTruthGather(float burstDistance)
+ {
+ if (completionPhase != CompletionPhase.FocusHolding)
+ return;
+
+ DOTween.Kill(TruthGatherTweenId);
+ truthGatherProgress = 0f;
+ foreach (TargetFocusVisual visual in focusVisuals.Values)
+ {
+ if (visual?.particle == null)
+ continue;
+
+ visual.gatherLocked = false;
+ Vector2 direction = UnityEngine.Random.insideUnitCircle.normalized;
+ visual.scatterOffset = (Vector3)(direction *
+ Mathf.Max(0f, burstDistance) *
+ UnityEngine.Random.Range(0.6f, 1.3f));
+ visual.particle.isStatic = false;
+ visual.particle.SetChangeInterval(0.05f);
+ visual.particle.ResetChangeTimer(0.05f);
+ visual.particle.anxietyShakeIntensity = Mathf.Max(visual.particle.anxietyShakeIntensity, 6f);
}
}
@@ -1731,6 +1819,8 @@ namespace AibisDream.MiniGame.Language
public void PrepareTruthReleaseFromLie()
{
DOTween.Kill(LieCompressionTweenId);
+ DOTween.Kill(TruthGatherTweenId);
+ truthGatherProgress = 0f;
foreach (TargetFocusVisual visual in focusVisuals.Values)
{
if (visual?.particle == null)
@@ -1806,10 +1896,27 @@ namespace AibisDream.MiniGame.Language
public void SetTruthWarmVisuals(Color warmColor)
{
+ ApplyPresentationResolvedVisuals(warmColor);
+ }
+
+ ///
+ /// 梳理完成/真话阶段:目标字改为指定色(默认白),并取消加粗以便大屏辨认。
+ /// 玩法阶段颜色不受影响;下一次 start_expression 会通过 RestoreTargetPresentationDefaults 还原。
+ ///
+ public void ApplyPresentationResolvedVisuals(Color resolvedColor)
+ {
+ Color color = resolvedColor;
+ color.a = 1f;
foreach (CandidateParticle particle in targetParticles)
{
if (particle == null) continue;
- particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, warmColor);
+ particle.SetColors(color, nonTargetParticleColor, nonTargetParticleColor, color);
+ particle.calmProgress = 1f;
+ particle.SetFontStyles(
+ targetParticleFontSize,
+ nonTargetParticleFontSize,
+ false,
+ nonTargetParticleBold);
}
}
@@ -1900,6 +2007,8 @@ namespace AibisDream.MiniGame.Language
}
DOTween.Kill(LieCompressionTweenId);
+ DOTween.Kill(TruthGatherTweenId);
+ truthGatherProgress = 0f;
StopSlotMachineShuffle();
completionPhase = CompletionPhase.Focusing;
focusTweensCompleted = true;
@@ -1932,6 +2041,8 @@ namespace AibisDream.MiniGame.Language
public void RestoreTargetPresentationDefaults(float alphaValue = 1f)
{
DOTween.Kill(LieCompressionTweenId);
+ DOTween.Kill(TruthGatherTweenId);
+ truthGatherProgress = 0f;
StopSlotMachineShuffle();
foreach (CandidateParticle particle in targetParticles)
{
@@ -1940,6 +2051,11 @@ namespace AibisDream.MiniGame.Language
particle.alpha = Mathf.Clamp01(alphaValue);
particle.calmProgress = 0f;
particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor);
+ particle.SetFontStyles(
+ targetParticleFontSize,
+ nonTargetParticleFontSize,
+ targetParticleBold,
+ nonTargetParticleBold);
}
resolveTotalDurationOverride = -1f;
lieEdgeCompression = 0f;
@@ -1997,6 +2113,7 @@ namespace AibisDream.MiniGame.Language
lieEdgeCompression = 0f;
lieEdgeJitter = 0f;
+ truthGatherProgress = 0f;
completionPhase = CompletionPhase.Finished;
}
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
index 69ddc6bc2..c15994ad1 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
@@ -1,4 +1,4 @@
-using System.Collections;
+using System.Collections;
using System.Collections.Generic;
using AibisDream.FixSystem;
using AibisDream;
@@ -179,28 +179,44 @@ namespace AibisDream.MiniGame.Language
yield return ExpressionManager.StartCoroutine(ExpressionManager.TransitionGlitchAndWait(to, duration));
}
+ ///
+ /// 记忆淡入(非阻塞):透明度在 fadeDuration 内显现,画面同时按 pushSpeed 持续推近;
+ /// targetOpacity 是本次淡入的目标透明度。horizontalSpeed 为旧 Yarn 兼容参数,基础层不再横移。
+ /// 争夺段的 jolt / tear / impact 仍会叠加。
+ ///
[YarnCommand("expression_memory_begin")]
- public static IEnumerator ExpressionMemoryBegin(
+ public static void ExpressionMemoryBegin(
string memoryKey,
- string preset,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
- bool preserveGlitch = false)
+ bool preserveGlitch = false,
+ float targetOpacity = 1f)
{
if (ExpressionManager == null)
{
UnityEngine.Debug.LogError("ExpressionManager 未找到!");
- yield break;
+ return;
}
- yield return ExpressionManager.StartCoroutine(
+ ExpressionManager.StartCoroutine(
ExpressionManager.BeginExpressionMemory(
memoryKey,
- preset,
fadeDuration,
pushSpeed,
horizontalSpeed,
- preserveGlitch));
+ preserveGlitch,
+ targetOpacity));
+ }
+
+ ///
+ /// 随对白把当前记忆透明度推进到目标值(非阻塞)。
+ /// <>
+ ///
+ [YarnCommand("expression_memory_alpha")]
+ public static void ExpressionMemoryAlpha(float target, float duration = 0.4f)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.SetActiveMemoryOpacity(target, duration);
}
///
@@ -315,6 +331,85 @@ namespace AibisDream.MiniGame.Language
presentation.PlayMemoryJolt(offset, duration, tear);
}
+ ///
+ /// 调节记忆的轻微失真扰动(非阻塞)。默认强度由演出预设决定,本命令用于演出中微调。
+ /// <>
+ ///
+ [YarnCommand("expression_memory_sim")]
+ public static void ExpressionMemorySim(float target, float duration = 0.3f)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.SetSimulationGlitch(target, duration);
+ }
+
+ ///
+ /// 真话聚合尝试(非阻塞):目标字被慢慢拉向最终位置并按字序逐个锁定成真心话。
+ /// target 小于 1 时最后几个字永远差一点锁不住——"感觉快要成功了"。
+ /// <>
+ ///
+ [YarnCommand("expression_truth_gather")]
+ public static void ExpressionTruthGather(float target = 0.9f, float duration = 3f)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.BeginTruthGatherAttempt(target, duration);
+ }
+
+ ///
+ /// 真话聚合被打断(非阻塞):目标字向外爆散并恢复乱跳。burst 为世界单位的爆散距离。
+ /// <>
+ ///
+ [YarnCommand("expression_truth_scatter")]
+ public static void ExpressionTruthScatter(float burst = 0.9f)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.ScatterTruthGatherAttempt(burst);
+ }
+
+ ///
+ /// 火山脸错误闪红(非阻塞):聚合失败时短暂替换红屏 Sprite,随后恢复,并轻微下沉。
+ /// <>
+ ///
+ [YarnCommand("expression_face_error")]
+ public static void ExpressionFaceError(float duration = 0.5f, float intensity = 0.85f)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.PlayFaceErrorFlash(duration, intensity);
+ }
+
+ ///
+ /// 结尾真假快速闪切(非阻塞,节奏由同长度 wait 控制):真心话与谎话交替独占屏幕,
+ /// 两者的屏幕底色(电视头背景)不同,越到后面切得越快,结束定格在真话帧。
+ /// <>
+ ///
+ [YarnCommand("expression_truth_lie_flicker")]
+ public static void ExpressionTruthLieFlicker(
+ string liePhrases,
+ string truthPhrase,
+ float duration,
+ float interval = 0.24f,
+ float fontSize = 12f)
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.StartTruthLieFlicker(liePhrases, truthPhrase, duration, interval, fontSize);
+ }
+
+ ///
+ /// 定格的谎话被 glitch 翻转成真话(阻塞):大字乱跳中长度从谎话渐变到真话、真假底色抢闪,
+ /// 最后落在暖色真话。紧接 expression_memory_cut + expression_truth_snap 收尾。
+ /// <>
+ ///
+ [YarnCommand("expression_lie_morph_truth")]
+ public static IEnumerator ExpressionLieMorphTruth(
+ string lieText,
+ string truthText,
+ float duration = 0.9f)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.MorphLieToTruth(lieText, truthText, duration));
+ }
+
[YarnCommand("expression_truth_leak")]
public static IEnumerator ExpressionTruthLeak(string fragment, float duration)
{
@@ -336,7 +431,7 @@ namespace AibisDream.MiniGame.Language
///
/// 演员大字闪现(阻塞):抖动的粒子字瞬间隐藏,屏幕中央出现白色大字(红青色散双影+急速缩放),
- /// 停顿 holdDuration 秒后恢复粒子字抖动。
+ /// 停顿 holdDuration 秒后始终恢复粒子字抖动;结尾定格请使用 expression_actor_flash_in。
/// <>
///
[YarnCommand("expression_actor_flash")]
@@ -366,19 +461,95 @@ namespace AibisDream.MiniGame.Language
glitchPeak));
}
+ ///
+ /// 演员大字入场并保持(阻塞):入场表现与 expression_actor_flash 相同,但不执行退场,
+ /// 适合在段落结尾定格。后续清场命令仍会正常移除它。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_flash_in")]
+ public static IEnumerator ExpressionActorFlashIn(
+ string text,
+ float holdDuration = 0.35f,
+ 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,
+ true));
+ }
+
+ ///
+ /// 非阻塞启动“没问题 → 冇问题 → 帽问题”三拍无限闪现循环。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_flash_loop_start")]
+ public static void ExpressionActorFlashLoopStart()
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.StartActorFlashLoop();
+ }
+
+ ///
+ /// 立即停止无限闪现循环、清除当前大字,并恢复晃动粒子字。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_flash_loop_stop")]
+ public static void ExpressionActorFlashLoopStop()
+ {
+ if (TryGetPresentation(out LogReleasePresentationController presentation))
+ presentation.StopActorFlashLoop();
+ }
+
+ ///
+ /// 真话闪现(阻塞):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍;
+ /// "被掐断"由紧随其后的 expression_glitch_pulse 表现。不推进谎话冲击等级。
+ /// <>
+ ///
+ [YarnCommand("expression_actor_truth_flash")]
+ public static IEnumerator ExpressionActorTruthFlash(
+ string text,
+ float holdDuration = 0.35f,
+ float fontSize = 8.5f,
+ float alpha = 0.8f,
+ float jitter = 0.02f)
+ {
+ if (!TryGetPresentation(out LogReleasePresentationController presentation))
+ yield break;
+ yield return ExpressionManager.StartCoroutine(
+ presentation.FlashActorTruth(text, holdDuration, fontSize, alpha, jitter));
+ }
+
///
/// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。
- /// <>
+ /// truthPool 非空时其字符也混入换字池,并以真话暖色显示。
+ /// <>
///
[YarnCommand("expression_actor_slot")]
public static void ExpressionActorSlot(
string charPool,
float duration,
float interval = 0.05f,
- float fontSize = 14f)
+ float fontSize = 14f,
+ string truthPool = "")
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
- presentation.StartActorSlotMachine(charPool, duration, interval, fontSize);
+ presentation.StartActorSlotMachine(charPool, duration, interval, fontSize, truthPool);
}
///
@@ -588,4 +759,3 @@ namespace AibisDream.MiniGame.Language
// }
}
}
-
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs
index b10909ace..f9d8e37ec 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LogReleasePresentationController.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Text;
using DG.Tweening;
using TMPro;
using UnityEngine;
@@ -69,6 +70,7 @@ namespace AibisDream.MiniGame.Language
[Header("Scene References")]
[SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private SpriteRenderer faceRenderer;
+ [SerializeField] private Sprite errorFaceSprite;
[SerializeField] private SpriteNoiseGlitchController glitchController;
[SerializeField] private RectTransform expressionScreenMaskRect;
[SerializeField] private TMP_FontAsset screenFont;
@@ -79,6 +81,12 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private Sprite privateOfficeMemory;
[SerializeField] private Material memoryMaterial;
+ [Header("Memory Simulation")]
+ [Tooltip("三档演出预设下记忆的轻微失真强度;随记忆透明度一起浮现。")]
+ [SerializeField, Range(0f, 1f)] private float simGlitchLight = 0.20f;
+ [SerializeField, Range(0f, 1f)] private float simGlitchMedium = 0.26f;
+ [SerializeField, Range(0f, 1f)] private float simGlitchHeavy = 0.32f;
+
[Header("Timing")]
[SerializeField, Min(0.01f)] private float faceFadeDuration = 0.45f;
[SerializeField, Min(0.01f)] private float memoryFadeDuration = 0.55f;
@@ -88,7 +96,15 @@ namespace AibisDream.MiniGame.Language
[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 Color truthWarmColor = new Color(1f, 1f, 1f, 1f);
+ [Tooltip("爆发段真话闪现与老虎机真话字符的暖色;需与谎话的冷白明显区分。")]
+ [SerializeField] private Color truthAccentColor = new Color(1f, 0.84f, 0.6f, 1f);
+ [Tooltip("结尾真假闪切时真心话帧的屏幕底色(电视头背景)。")]
+ [SerializeField] private Color truthBackdropColor = new Color(0.30f, 0.20f, 0.10f, 0.78f);
+ [Tooltip("结尾真假闪切时谎话帧的屏幕底色。")]
+ [SerializeField] private Color lieBackdropColor = new Color(0.02f, 0.05f, 0.09f, 0.82f);
+ [Tooltip("梳理完成后目标粒子与真话字的颜色;玩法阶段仍用粒子管理器原色。")]
+ [SerializeField] private Color presentationResolvedColor = Color.white;
[SerializeField] private int screenGlowSortingOrder = 8;
[SerializeField] private int screenTextSortingOrder = 18;
@@ -111,6 +127,12 @@ namespace AibisDream.MiniGame.Language
private TextMeshPro actorFlashText;
private TextMeshPro actorFlashChromaRed;
private TextMeshPro actorFlashChromaCyan;
+ private TextMeshPro truthFlashText;
+ private TMPRectClipper truthFlashClipper;
+ private bool truthFlashActive;
+ private SpriteRenderer flickerBackdropRenderer;
+ private Coroutine truthLieFlickerRoutine;
+ private string truthWarmColorHex;
private TMPRectClipper lieClipper;
private TMPRectClipper lieGhostClipper;
private TMPRectClipper systemClipper;
@@ -122,6 +144,7 @@ namespace AibisDream.MiniGame.Language
private bool actorFlashActive;
private int actorFlashCount;
private Coroutine actorOverdriveRoutine;
+ private Coroutine actorFlashLoopRoutine;
private Texture2D runtimeWhiteTexture;
private Sprite runtimeWhiteSprite;
@@ -134,6 +157,7 @@ namespace AibisDream.MiniGame.Language
private float contrast = 1f;
private float vignette;
private float noise;
+ private float simGlitch;
private float horizontalTear;
private float memoryImpact;
private float memoryDamage;
@@ -144,22 +168,23 @@ namespace AibisDream.MiniGame.Language
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 memoryPushProgress;
private float memoryZoom = 1f;
private Vector3 memoryBasePosition;
private Vector3 memoryBaseScale;
private Vector3 faceBasePosition;
+ private Sprite normalFaceSprite;
private bool runtimeObjectsReady;
private Coroutine lieShuffleRoutine;
+ private Coroutine faceErrorRoutine;
private static readonly int SaturationId = Shader.PropertyToID("_Saturation");
private static readonly int BrightnessId = Shader.PropertyToID("_Brightness");
private static readonly int ContrastId = Shader.PropertyToID("_Contrast");
private static readonly int VignetteId = Shader.PropertyToID("_Vignette");
private static readonly int NoiseId = Shader.PropertyToID("_Noise");
+ private static readonly int SimGlitchId = Shader.PropertyToID("_SimGlitch");
private static readonly int HorizontalTearId = Shader.PropertyToID("_HorizontalTear");
private static readonly int MemoryImpactId = Shader.PropertyToID("_Impact");
private static readonly int MemoryDamageId = Shader.PropertyToID("_Damage");
@@ -178,6 +203,7 @@ namespace AibisDream.MiniGame.Language
{
EnsureRuntimeObjects();
faceBasePosition = faceRenderer != null ? faceRenderer.transform.position : Vector3.zero;
+ normalFaceSprite = faceRenderer != null ? faceRenderer.sprite : null;
ForceCleanupImmediate(true);
}
@@ -186,20 +212,10 @@ namespace AibisDream.MiniGame.Language
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);
- }
+ memoryPushProgress += Time.deltaTime * memoryPushSpeed;
+ activeMemoryRenderer.transform.localScale =
+ memoryBaseScale * (1f + memoryPushProgress) * memoryZoom;
+ activeMemoryRenderer.transform.position = memoryBasePosition;
ApplyMemoryBlocks();
}
@@ -213,6 +229,7 @@ namespace AibisDream.MiniGame.Language
memoryOpacityB > 0.001f ||
(lieText != null && lieText.gameObject.activeSelf) ||
(actorFlashText != null && actorFlashText.gameObject.activeSelf) ||
+ (truthFlashText != null && truthFlashText.gameObject.activeSelf) ||
actorScrollTexts.Exists(text => text != null && text.gameObject.activeSelf);
DOTween.Kill(this);
@@ -235,6 +252,7 @@ namespace AibisDream.MiniGame.Language
FadeText(actorFlashText, 0f, duration);
FadeText(actorFlashChromaRed, 0f, duration);
FadeText(actorFlashChromaCyan, 0f, duration);
+ FadeText(truthFlashText, 0f, duration);
foreach (TextMeshPro text in actorScrollTexts)
FadeText(text, 0f, duration);
if (faceRenderer != null && faceRenderer.gameObject.activeSelf)
@@ -259,6 +277,8 @@ namespace AibisDream.MiniGame.Language
}
state = PresentationState.FaceEntering;
+ // 梳理完成起:目标字切到白色(并取消加粗),玩法阶段颜色不受影响。
+ particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor);
if (glitchController != null)
{
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
@@ -288,11 +308,11 @@ namespace AibisDream.MiniGame.Language
public IEnumerator BeginMemory(
string memoryKey,
- string presetName,
float fadeDuration = -1f,
float pushSpeed = -1f,
float horizontalSpeed = -1f,
- bool preserveGlitch = false)
+ bool preserveGlitch = false,
+ float targetOpacity = 1f)
{
EnsureRuntimeObjects();
EnsureScreenTextClipMaterials();
@@ -301,11 +321,7 @@ namespace AibisDream.MiniGame.Language
Debug.LogError($"[LogReleasePresentation] 未知记忆图片“{memoryKey}”;命令安全结束。");
yield break;
}
- if (!TryResolvePreset(presetName, out Preset preset))
- {
- Debug.LogError($"[LogReleasePresentation] 未知演出预设“{presetName}”;命令安全结束。");
- yield break;
- }
+ Preset preset = GetPresetForMemory(kind);
if (!ExpectState(
nameof(BeginMemory),
@@ -319,12 +335,15 @@ namespace AibisDream.MiniGame.Language
currentPreset = preset;
currentMemoryKind = kind;
currentLeakFragment = GetLeakFragment(kind);
+ _ = horizontalSpeed;
actorFlashCount = 0;
memoryImpact = 0f;
memoryDamage = 0f;
memoryOverdrive = 0f;
memoryTearSeed = 0f;
state = PresentationState.Memory;
+ // 兜底:跳过 face_fade_in 直进记忆时,目标字仍切到演出白。
+ particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor);
if (glitchController != null)
{
@@ -344,28 +363,41 @@ namespace AibisDream.MiniGame.Language
activeMemoryRenderer = incoming;
ConfigureMemoryLook(kind, preset);
- memoryMotionTime = 0f;
- memoryMotionWeight = 1f;
- memoryZoom = 1f;
+ memoryBasePosition = incoming.transform.position;
+ memoryBaseScale = incoming.transform.localScale;
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;
+ memoryPushProgress = 0f;
+ memoryZoom = 1f;
float resolvedFadeDuration = fadeDuration > 0f
? Mathf.Max(0.01f, fadeDuration)
: memoryFadeDuration;
SetMemoryOpacity(incoming, 0f);
- TweenMemoryOpacity(incoming, 1f, resolvedFadeDuration);
+ TweenMemoryOpacity(
+ incoming,
+ Mathf.Clamp01(targetOpacity),
+ resolvedFadeDuration,
+ Ease.Linear);
if (outgoing != null && outgoing != incoming)
TweenMemoryOpacity(outgoing, 0f, resolvedFadeDuration);
yield return new WaitForSeconds(resolvedFadeDuration);
}
+ /// 把当前记忆透明度推进到目标值(非等待),用于随对白逐段显影。
+ public void SetActiveMemoryOpacity(float target, float duration)
+ {
+ EnsureRuntimeObjects();
+ if (activeMemoryRenderer == null || !activeMemoryRenderer.enabled)
+ return;
+
+ TweenMemoryOpacity(
+ activeMemoryRenderer,
+ Mathf.Clamp01(target),
+ Mathf.Max(0.01f, duration));
+ }
+
public IEnumerator BeginLie(string keyword, string presetName)
{
EnsureRuntimeObjects();
@@ -453,10 +485,17 @@ namespace AibisDream.MiniGame.Language
state = PresentationState.LieHolding;
DOTween.Kill(this);
float duration = Mathf.Max(0.01f, freezeDuration);
- DOTween.To(() => memoryMotionWeight, value => memoryMotionWeight = value, 0f, duration)
+ // 谎话期间把模拟感压到目标值:记忆被伪装得"过分干净"。
+ DOTween.To(() => simGlitch, value => simGlitch = value, Mathf.Clamp01(noiseTarget), duration)
.SetEase(Ease.OutQuad)
.SetTarget(this);
- DOTween.To(() => noise, value => noise = value, Mathf.Clamp01(noiseTarget), duration)
+ }
+
+ /// 调节记忆的轻微失真扰动(非等待)。
+ public void SetSimulationGlitch(float target, float duration)
+ {
+ EnsureRuntimeObjects();
+ DOTween.To(() => simGlitch, value => simGlitch = value, Mathf.Clamp01(target), Mathf.Max(0.01f, duration))
.SetEase(Ease.OutQuad)
.SetTarget(this);
}
@@ -522,6 +561,8 @@ namespace AibisDream.MiniGame.Language
EnsureRuntimeObjects();
if (!ExpectState(
nameof(ShowSystemMessage),
+ PresentationState.FaceEntering,
+ PresentationState.Memory,
PresentationState.LieHolding,
PresentationState.LieCracking))
return;
@@ -642,13 +683,16 @@ namespace AibisDream.MiniGame.Language
return;
DOTween.Kill(this);
+ StopActorFlashLoopImmediate(false);
StopActorOverdriveImmediate();
- memoryMotionWeight = 0f;
+ memoryPushSpeed = 0f;
+ memoryPushProgress = 0f;
memoryZoom = 1f;
memoryOpacityA = 0f;
memoryOpacityB = 0f;
horizontalTear = 0f;
noise = 0f;
+ simGlitch = 0f;
effectStrength = 0f;
memoryImpact = 0f;
memoryDamage = 0f;
@@ -657,6 +701,7 @@ namespace AibisDream.MiniGame.Language
DisableMemoryRenderer(memoryRendererA);
DisableMemoryRenderer(memoryRendererB);
activeMemoryRenderer = null;
+ StopTruthLieFlicker();
if (glitchController != null)
{
@@ -669,7 +714,8 @@ namespace AibisDream.MiniGame.Language
///
/// 演员大字闪现:glitch 一拍的瞬间隐藏抖动的粒子字,屏幕中央出现白色大字。
- /// 三次冲击逐级增强并累积记忆损伤;第三次大字不再退回粒子,而是留给老虎机过载态接管。
+ /// 三次冲击逐级增强并累积记忆损伤;普通闪现结束后始终退回抖动粒子字。
+ /// 只有 expression_actor_flash_in 会保持大字,用于明确的结尾定格。
///
public IEnumerator FlashActorLie(
string text,
@@ -680,7 +726,8 @@ namespace AibisDream.MiniGame.Language
float chromaDistance = -1f,
float chromaAlpha = -1f,
float impact = -1f,
- float glitchPeak = -1f)
+ float glitchPeak = -1f,
+ bool keepVisible = false)
{
EnsureRuntimeObjects();
EnsureScreenTextClipMaterials();
@@ -693,6 +740,7 @@ namespace AibisDream.MiniGame.Language
}
actorFlashActive = true;
+ PulseFaceErrorSprite(Mathf.Max(0.1f, holdDuration));
actorFlashCount = Mathf.Clamp(actorFlashCount + 1, 1, 3);
int flashLevel = actorFlashCount;
float flashProgress = flashLevel / 3f;
@@ -702,10 +750,10 @@ namespace AibisDream.MiniGame.Language
: Mathf.Lerp(0.16f, 0.42f, flashProgress);
float resolvedChromaAlpha = chromaAlpha >= 0f
? Mathf.Clamp01(chromaAlpha)
- : Mathf.Lerp(0.22f, 0.36f, flashProgress);
+ : Mathf.Lerp(0.06f, 0.12f, flashProgress);
float resolvedChromaDistance = chromaDistance >= 0f
? chromaDistance
- : Mathf.Lerp(0.025f, 0.065f, flashProgress);
+ : Mathf.Lerp(0.006f, 0.014f, flashProgress);
float resolvedSettledScale = settledScaleValue > 0f
? settledScaleValue
: Mathf.Lerp(1.15f, 1.25f, flashProgress);
@@ -756,7 +804,7 @@ namespace AibisDream.MiniGame.Language
finally
{
actorFlashActive = false;
- bool holdForOverdrive = flashLevel >= 3 && state == PresentationState.Memory;
+ bool holdForOverdrive = keepVisible;
if (!holdForOverdrive)
HideActorFlashVisualsImmediate();
if (!holdForOverdrive && state == PresentationState.Memory)
@@ -764,14 +812,127 @@ namespace AibisDream.MiniGame.Language
}
}
+ public void StartActorFlashLoop()
+ {
+ EnsureRuntimeObjects();
+ if (!ExpectState(nameof(StartActorFlashLoop), PresentationState.Memory))
+ return;
+
+ StopActorFlashLoopImmediate(true);
+ actorFlashLoopRoutine = StartCoroutine(ActorFlashLoopRoutine());
+ }
+
+ public void StopActorFlashLoop()
+ {
+ StopActorFlashLoopImmediate(true);
+ }
+
+ private IEnumerator ActorFlashLoopRoutine()
+ {
+ while (state == PresentationState.Memory)
+ {
+ yield return FlashActorLie(
+ "没问题", 0.25f, 10.5f, 1.1f, 1.45f, 0.008f, 0.06f, 0.50f, 0f);
+ if (state != PresentationState.Memory)
+ break;
+ yield return new WaitForSeconds(0.3f);
+
+ yield return FlashActorLie(
+ "冇问题", 0.25f, 11.2f, 1.1f, 1.85f, 0.008f, 0.06f, 0.72f, 0f);
+ if (state != PresentationState.Memory)
+ break;
+ yield return new WaitForSeconds(0.3f);
+
+ yield return FlashActorLie(
+ "帽问题", 0.15f, 12.0f, 1.1f, 2.20f, 0.010f, 0.08f, 1.00f, 0f);
+ if (state != PresentationState.Memory)
+ break;
+ yield return new WaitForSeconds(0.3f);
+ }
+
+ actorFlashLoopRoutine = null;
+ }
+
+ private void StopActorFlashLoopImmediate(bool restoreParticles)
+ {
+ if (actorFlashLoopRoutine != null)
+ {
+ StopCoroutine(actorFlashLoopRoutine);
+ actorFlashLoopRoutine = null;
+ }
+
+ actorFlashActive = false;
+ memoryImpact = 0f;
+ HideActorFlashVisualsImmediate();
+ if (restoreParticles && state == PresentationState.Memory)
+ particleManager?.SetTargetParticlesAlpha(1f, 0f);
+ }
+
+ ///
+ /// 真话闪现:谎话大字之间,暖色完整真话带着细微抖动挣扎浮现一拍。
+ /// 不推进谎话冲击等级、不累积记忆损伤;"被掐断"由紧随其后的 expression_glitch_pulse 表现。
+ ///
+ public IEnumerator FlashActorTruth(
+ string text,
+ float holdDuration,
+ float fontSize = 8.5f,
+ float alpha = 0.8f,
+ float jitter = 0.02f)
+ {
+ EnsureRuntimeObjects();
+ EnsureScreenTextClipMaterials();
+ if (!ExpectState(nameof(FlashActorTruth), PresentationState.Memory))
+ yield break;
+ if (truthFlashActive)
+ {
+ Debug.LogWarning("[LogReleasePresentation] expression_actor_truth_flash 在上一次闪现结束前被调用;已安全跳过。");
+ yield break;
+ }
+
+ truthFlashActive = true;
+ try
+ {
+ PositionScreenText();
+ string value = text ?? string.Empty;
+ truthFlashText.fontSize = Mathf.Max(0.1f, fontSize);
+ Color warm = truthAccentColor;
+ warm.a = 0f;
+ SetTextActive(truthFlashText, value, warm);
+ truthFlashText.transform.localScale = Vector3.one;
+ FadeText(truthFlashText, Mathf.Clamp01(alpha), 0.09f);
+
+ Vector3 origin = truthFlashText.transform.position;
+ float shake = Mathf.Max(0f, jitter);
+ float elapsed = 0f;
+ float hold = Mathf.Max(0.05f, holdDuration);
+ while (elapsed < hold && state == PresentationState.Memory)
+ {
+ truthFlashText.transform.position = origin + new Vector3(
+ UnityEngine.Random.Range(-shake, shake),
+ UnityEngine.Random.Range(-shake, shake) * 0.6f,
+ 0f);
+ elapsed += Time.deltaTime;
+ yield return null;
+ }
+ truthFlashText.transform.position = origin;
+ }
+ finally
+ {
+ truthFlashActive = false;
+ HideTextImmediate(truthFlashText);
+ }
+ }
+
///
/// 老虎机换字演出:聚焦字在 duration 内只从 charPool 里疯狂换字(非阻塞,节拍由 Yarn wait 控制)。
+ /// truthPool 非空时其字符也混入换字池,并在主字上以真话暖色显示——真假字符交替闪烁。
///
public void StartActorSlotMachine(
string charPool,
float duration,
float interval,
- float fontSize = 14f)
+ float fontSize = 14f,
+ string truthPool = "")
{
EnsureRuntimeObjects();
if (!ExpectState(nameof(StartActorSlotMachine), PresentationState.Memory))
@@ -782,6 +943,8 @@ namespace AibisDream.MiniGame.Language
Debug.LogWarning("[LogReleasePresentation] 演员老虎机字符池为空;已安全跳过。");
return;
}
+ string truthCharacters = LanguageParticleManager.SanitizeSlotMachinePool(truthPool) ?? string.Empty;
+ pool += truthCharacters;
StopActorOverdriveImmediate();
particleManager?.SetTargetParticlesAlpha(0f, 0f);
@@ -792,7 +955,8 @@ namespace AibisDream.MiniGame.Language
actorOverdriveRoutine = StartCoroutine(ActorOverdriveRoutine(
pool,
Mathf.Max(0.05f, duration),
- Mathf.Max(0.02f, interval)));
+ Mathf.Max(0.02f, interval),
+ truthCharacters));
if (glitchController != null)
{
StartCoroutine(PulseGlitch(
@@ -817,6 +981,7 @@ namespace AibisDream.MiniGame.Language
HideActorScrollImmediate();
HideActorFlashVisualsImmediate();
particleManager?.SetTargetParticlesAlpha(0f, 0f);
+ PulseFaceErrorSprite(Mathf.Max(0.1f, holdDuration));
PositionScreenText();
string value = text ?? string.Empty;
@@ -935,7 +1100,7 @@ namespace AibisDream.MiniGame.Language
StopScreenDim();
particleManager?.PrepareTruthReleaseFromLie();
- particleManager?.SetTruthWarmVisuals(truthWarmColor);
+ particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor);
particleManager?.SetTargetParticlesAlpha(1f, 0f);
Bounds truthBounds = particleManager != null
? particleManager.GetFinalTruthWorldBounds()
@@ -947,10 +1112,261 @@ namespace AibisDream.MiniGame.Language
if (glitchController != null)
glitchController.Intensity = 0f;
- memoryMotionWeight = 0f;
state = PresentationState.TruthHolding;
}
+ /// 真话聚合尝试(非等待):目标字被慢慢拉向最终位置并按序逐字锁定,感觉即将成功。
+ public void BeginTruthGatherAttempt(float target, float duration)
+ {
+ if (!ExpectState(nameof(BeginTruthGatherAttempt), PresentationState.Memory))
+ return;
+ particleManager?.BeginTruthGather(target, duration);
+ }
+
+ /// 真话聚合被打断:目标字向外爆散并恢复乱跳(非等待)。
+ public void ScatterTruthGatherAttempt(float burstDistance)
+ {
+ if (!ExpectState(nameof(ScatterTruthGatherAttempt), PresentationState.Memory))
+ return;
+ particleManager?.ScatterTruthGather(burstDistance);
+ }
+
+ /// 火山脸错误闪红:聚合失败的报错信号(非等待)。
+ public void PlayFaceErrorFlash(float duration, float intensity)
+ {
+ if (faceRenderer == null)
+ return;
+
+ _ = intensity; // 兼容旧 Yarn 参数;红屏 Sprite 本身决定最终颜色。
+ float total = Mathf.Max(0.1f, duration);
+ PulseFaceErrorSprite(total);
+ DipFace(total * 0.8f, faceDipRatio * 1.6f);
+ }
+
+ private void PulseFaceErrorSprite(float duration)
+ {
+ if (faceRenderer == null || errorFaceSprite == null)
+ return;
+
+ if (faceErrorRoutine != null)
+ StopCoroutine(faceErrorRoutine);
+ if (faceRenderer.sprite != errorFaceSprite)
+ normalFaceSprite = faceRenderer.sprite;
+ faceRenderer.sprite = errorFaceSprite;
+ faceErrorRoutine = StartCoroutine(RestoreFaceSpriteAfter(Mathf.Max(0.05f, duration)));
+ }
+
+ private IEnumerator RestoreFaceSpriteAfter(float duration)
+ {
+ yield return new WaitForSeconds(duration);
+ RestoreFaceSpriteImmediate();
+ }
+
+ private void RestoreFaceSpriteImmediate()
+ {
+ if (faceRenderer != null && normalFaceSprite != null)
+ faceRenderer.sprite = normalFaceSprite;
+ faceErrorRoutine = null;
+ }
+
+ ///
+ /// 结尾真假快速闪切(非等待):真心话与谎话交替独占屏幕,两者的屏幕底色(电视头背景)不同,
+ /// 越到后面切得越快,最后定格在真话帧。节奏由同长度的 Yarn wait 控制。
+ ///
+ public void StartTruthLieFlicker(
+ string liePhrases,
+ string truthPhrase,
+ float duration,
+ float interval,
+ float fontSize)
+ {
+ EnsureRuntimeObjects();
+ EnsureScreenTextClipMaterials();
+ if (!ExpectState(nameof(StartTruthLieFlicker), PresentationState.Memory))
+ return;
+ string[] lies = ParseActorPhrases(liePhrases);
+ if (lies.Length == 0 || string.IsNullOrEmpty(truthPhrase))
+ {
+ Debug.LogWarning("[LogReleasePresentation] 真假闪切词组为空;已安全跳过。");
+ return;
+ }
+
+ StopTruthLieFlicker();
+ StopActorOverdriveImmediate();
+ particleManager?.SetTargetParticlesAlpha(0f, 0f);
+ truthLieFlickerRoutine = StartCoroutine(TruthLieFlickerRoutine(
+ lies,
+ truthPhrase,
+ Mathf.Max(0.2f, duration),
+ Mathf.Max(0.05f, interval),
+ Mathf.Max(0.1f, fontSize)));
+ }
+
+ private IEnumerator TruthLieFlickerRoutine(
+ string[] lies,
+ string truthPhrase,
+ float duration,
+ float interval,
+ float fontSize)
+ {
+ PositionScreenText();
+ actorFlashText.fontSize = fontSize;
+ actorFlashText.transform.localScale = Vector3.one;
+ HideTextImmediate(actorFlashChromaRed);
+ HideTextImmediate(actorFlashChromaCyan);
+ ShowFlickerBackdrop();
+
+ bool truthTurn = false;
+ int lieIndex = 0;
+ float elapsed = 0f;
+ while (elapsed < duration && state == PresentationState.Memory)
+ {
+ ShowFlickerFrame(truthTurn, lies, lieIndex, truthPhrase);
+ if (!truthTurn)
+ lieIndex++;
+ truthTurn = !truthTurn;
+
+ // 越到后面切得越快:真话越来越压不住
+ float progress = Mathf.Clamp01(elapsed / duration);
+ float step = Mathf.Lerp(interval, interval * 0.45f, progress);
+ step = Mathf.Min(step, duration - elapsed);
+ elapsed += step;
+ yield return new WaitForSeconds(Mathf.Max(0.02f, step));
+ }
+
+ // 最后停在第一条谎话("没问题"),交给 expression_lie_morph_truth 做 glitch 翻转
+ if (state == PresentationState.Memory)
+ ShowFlickerFrame(false, lies, 0, truthPhrase);
+ truthLieFlickerRoutine = null;
+ }
+
+ private void ShowFlickerFrame(bool truth, string[] lies, int lieIndex, string truthPhrase)
+ {
+ if (truth)
+ {
+ Color warm = truthAccentColor;
+ warm.a = 1f;
+ SetTextActive(actorFlashText, truthPhrase, warm);
+ SetFlickerBackdropColor(truthBackdropColor);
+ }
+ else
+ {
+ SetTextActive(actorFlashText, lies[lieIndex % lies.Length], Color.white);
+ SetFlickerBackdropColor(lieBackdropColor);
+ }
+ }
+
+ private void ShowFlickerBackdrop()
+ {
+ if (flickerBackdropRenderer == null)
+ return;
+ Bounds screen = GetScreenBounds();
+ flickerBackdropRenderer.transform.position = screen.center;
+ flickerBackdropRenderer.transform.localScale = new Vector3(screen.size.x, screen.size.y, 1f);
+ flickerBackdropRenderer.enabled = true;
+ flickerBackdropRenderer.gameObject.SetActive(true);
+ }
+
+ private void SetFlickerBackdropColor(Color color)
+ {
+ if (flickerBackdropRenderer != null)
+ flickerBackdropRenderer.color = color;
+ }
+
+ private void HideFlickerBackdropImmediate()
+ {
+ if (flickerBackdropRenderer == null)
+ return;
+ flickerBackdropRenderer.enabled = false;
+ flickerBackdropRenderer.gameObject.SetActive(false);
+ }
+
+ private void StopTruthLieFlicker()
+ {
+ if (truthLieFlickerRoutine != null)
+ {
+ StopCoroutine(truthLieFlickerRoutine);
+ truthLieFlickerRoutine = null;
+ }
+ HideFlickerBackdropImmediate();
+ }
+
+ ///
+ /// 定格的谎话被 glitch 翻转成真话(等待):大字在乱跳中长度渐变、真假底色抢闪,
+ /// 最后落在暖色真话并轻微收缩定住。后续由 expression_memory_cut + expression_truth_snap 收尾。
+ ///
+ public IEnumerator MorphLieToTruth(string lieValue, string truthValue, float duration)
+ {
+ EnsureRuntimeObjects();
+ EnsureScreenTextClipMaterials();
+ if (!ExpectState(nameof(MorphLieToTruth), PresentationState.Memory))
+ yield break;
+
+ string lie = lieValue ?? string.Empty;
+ string truth = truthValue ?? string.Empty;
+ string pool = lie + truth;
+ if (pool.Length == 0 || truth.Length == 0)
+ {
+ Debug.LogWarning("[LogReleasePresentation] expression_lie_morph_truth 文本为空;已安全跳过。");
+ yield break;
+ }
+
+ if (truthLieFlickerRoutine != null)
+ {
+ StopCoroutine(truthLieFlickerRoutine);
+ truthLieFlickerRoutine = null;
+ }
+ StopActorOverdriveImmediate();
+ particleManager?.SetTargetParticlesAlpha(0f, 0f);
+ PositionScreenText();
+ ShowFlickerBackdrop();
+ HideTextImmediate(actorFlashChromaRed);
+ HideTextImmediate(actorFlashChromaCyan);
+
+ Vector3 origin = actorFlashText.transform.position;
+ SetTextActive(actorFlashText, lie, Color.white);
+ SetFlickerBackdropColor(lieBackdropColor);
+
+ float total = Mathf.Max(0.2f, duration);
+ float scrambleDuration = total * 0.7f;
+ float elapsed = 0f;
+ bool warmTick = false;
+ while (elapsed < scrambleDuration && state == PresentationState.Memory)
+ {
+ float progress = Mathf.Clamp01(elapsed / scrambleDuration);
+ int length = Mathf.Clamp(
+ Mathf.RoundToInt(Mathf.Lerp(Mathf.Max(1, lie.Length), truth.Length, progress)),
+ 1,
+ Mathf.Max(1, truth.Length));
+ warmTick = !warmTick;
+ Color color = warmTick ? truthAccentColor : Color.white;
+ color.a = 1f;
+ SetTextActive(actorFlashText, BuildRandomActorText(pool, length), color);
+ SetFlickerBackdropColor(warmTick ? truthBackdropColor : lieBackdropColor);
+ actorFlashText.transform.position = origin +
+ (Vector3)(UnityEngine.Random.insideUnitCircle * Mathf.Lerp(0.01f, 0.05f, progress));
+
+ float step = Mathf.Lerp(0.09f, 0.035f, progress);
+ step = Mathf.Min(step, scrambleDuration - elapsed);
+ elapsed += step;
+ yield return new WaitForSeconds(Mathf.Max(0.02f, step));
+ }
+
+ if (state != PresentationState.Memory)
+ yield break;
+
+ actorFlashText.transform.position = origin;
+ Color warm = truthAccentColor;
+ warm.a = 1f;
+ SetTextActive(actorFlashText, truth, warm);
+ SetFlickerBackdropColor(truthBackdropColor);
+ actorFlashText.transform.localScale = Vector3.one * 1.12f;
+ actorFlashText.transform.DOScale(Vector3.one, 0.12f)
+ .SetEase(Ease.OutCubic)
+ .SetTarget(this);
+ yield return new WaitForSeconds(Mathf.Max(0.05f, total - scrambleDuration));
+ }
+
public void PlayGlitchPulse(float peak, float riseDuration, float fallDuration)
{
if (!ExpectState(
@@ -1032,10 +1448,14 @@ namespace AibisDream.MiniGame.Language
{
if (!ExpectState(
nameof(HoldTruthBlackout),
+ PresentationState.Memory,
PresentationState.LieHolding,
PresentationState.LieCracking))
yield break;
+ // 顿挫一拍:黑场盖住真假闪切的底色与大字
+ StopTruthLieFlicker();
+ HideTextImmediate(actorFlashText);
HoldTruthBreakBlackout(alpha);
yield return new WaitForSeconds(Mathf.Max(0.01f, duration));
}
@@ -1144,7 +1564,7 @@ namespace AibisDream.MiniGame.Language
state = PresentationState.TruthResolving;
HideSystemMessage(0.15f);
particleManager?.PrepareTruthReleaseFromLie();
- particleManager?.SetTruthWarmVisuals(truthWarmColor);
+ particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor);
particleManager?.SetTargetParticlesAlpha(1f, Mathf.Max(0.01f, alphaDuration));
Bounds truthBounds = particleManager != null
? particleManager.GetFinalTruthWorldBounds()
@@ -1194,7 +1614,6 @@ namespace AibisDream.MiniGame.Language
ReleaseScreenTextClipMaterials();
if (glitchController != null)
glitchController.TransitionTo(0f, 0.18f);
- memoryMotionWeight = 0f;
state = PresentationState.TruthHolding;
}
@@ -1254,6 +1673,8 @@ namespace AibisDream.MiniGame.Language
{
DOTween.Kill(this);
StopAllCoroutines();
+ actorFlashLoopRoutine = null;
+ RestoreFaceSpriteImmediate();
memoryOpacityA = 0f;
memoryOpacityB = 0f;
@@ -1291,9 +1712,12 @@ namespace AibisDream.MiniGame.Language
state = PresentationState.Idle;
currentLieKeyword = string.Empty;
currentLeakFragment = string.Empty;
- memoryMotionWeight = 0f;
+ memoryPushSpeed = 0f;
+ memoryPushProgress = 0f;
memoryZoom = 1f;
horizontalTear = 0f;
+ noise = 0f;
+ simGlitch = 0f;
memoryImpact = 0f;
memoryDamage = 0f;
memoryOverdrive = 0f;
@@ -1346,7 +1770,7 @@ namespace AibisDream.MiniGame.Language
.WaitForCompletion();
}
- private IEnumerator ActorOverdriveRoutine(string pool, float duration, float interval)
+ private IEnumerator ActorOverdriveRoutine(string pool, float duration, float interval, string truthPool = "")
{
PositionScreenText();
string initial = actorFlashText != null ? actorFlashText.text : string.Empty;
@@ -1358,13 +1782,14 @@ namespace AibisDream.MiniGame.Language
{
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));
+ // 起点压低,让老虎机段有"从可辨认到失控"的爬升;峰值仍到 1
+ memoryOverdrive = Mathf.Lerp(0.15f, 1f, eased);
+ memoryDamage = Mathf.Max(memoryDamage, Mathf.Lerp(0.45f, 1f, eased));
if (elapsed >= nextShuffle)
{
string value = BuildRandomActorText(pool, characterCount);
- SetActorOverdriveText(value, eased);
+ SetActorOverdriveText(value, eased, truthPool);
memoryTearSeed = Mathf.Repeat(memoryTearSeed + 0.173f, 1f);
float acceleratingInterval = Mathf.Lerp(
interval,
@@ -1395,6 +1820,21 @@ namespace AibisDream.MiniGame.Language
actorOverdriveRoutine = null;
}
+ public static string ColorizeTruthCharacters(string value, string truthPool, string colorHex)
+ {
+ if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(truthPool))
+ return value;
+ var builder = new StringBuilder(value.Length * 8);
+ foreach (char character in value)
+ {
+ if (truthPool.IndexOf(character) >= 0)
+ builder.Append("').Append(character).Append("");
+ else
+ builder.Append(character);
+ }
+ return builder.ToString();
+ }
+
private static string BuildRandomActorText(string pool, int characterCount)
{
if (string.IsNullOrEmpty(pool))
@@ -1406,21 +1846,25 @@ namespace AibisDream.MiniGame.Language
return new string(value);
}
- private void SetActorOverdriveText(string value, float progress)
+ private void SetActorOverdriveText(string value, float progress, string truthPool = "")
{
- SetTextActive(actorFlashText, value, Color.white);
+ // 主字用富文本按字符归属着色:真话字符=暖色,谎话字符=白。ghost 层保持整体色。
+ truthWarmColorHex ??= ColorUtility.ToHtmlStringRGB(truthAccentColor);
+ string mainValue = ColorizeTruthCharacters(value, truthPool, truthWarmColorHex);
+ SetTextActive(actorFlashText, mainValue, Color.white);
SetTextActive(
actorFlashChromaRed,
value,
- new Color(1f, 0.18f, 0.18f, Mathf.Lerp(0.22f, 0.38f, progress)));
+ new Color(1f, 0.28f, 0.28f, Mathf.Lerp(0.05f, 0.14f, progress)));
SetTextActive(
actorFlashChromaCyan,
value,
- new Color(0.16f, 0.92f, 1f, Mathf.Lerp(0.22f, 0.38f, progress)));
+ new Color(0.25f, 0.9f, 1f, Mathf.Lerp(0.05f, 0.14f, 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;
+ // 过载段保留轻色散即可,避免主体字被红青双影糊掉
+ float chromaDistance = Mathf.Lerp(0.008f, 0.028f, progress);
+ float verticalJitter = UnityEngine.Random.Range(-0.004f, 0.004f) * progress;
actorFlashChromaRed.transform.position =
center + Vector3.left * chromaDistance + Vector3.up * verticalJitter;
actorFlashChromaCyan.transform.position =
@@ -1598,18 +2042,25 @@ namespace AibisDream.MiniGame.Language
};
brightness = kind switch
{
- MemoryKind.Office => 0.88f,
+ MemoryKind.Office => 0.94f,
MemoryKind.Sunset => 0.94f,
_ => 0.76f
};
contrast = kind == MemoryKind.PrivateOffice ? 1.20f : 1.08f;
vignette = kind switch
{
- MemoryKind.Office => 0.24f,
+ MemoryKind.Office => 0.18f,
MemoryKind.Sunset => 0.18f,
_ => 0.46f
};
- noise = 0.04f + preset.BaseDistortion * 0.28f;
+ // 颗粒噪点弃用;仅由 _SimGlitch 保留轻微 UV 失真扰动。
+ noise = 0f;
+ simGlitch = preset.Name switch
+ {
+ "Medium" => simGlitchMedium,
+ "Heavy" => simGlitchHeavy,
+ _ => simGlitchLight
+ };
horizontalTear = 0f;
memoryTint = kind switch
{
@@ -1665,18 +2116,21 @@ namespace AibisDream.MiniGame.Language
actorFlashText = CreateScreenText("ActorFlashKeyword", 10.5f, screenTextSortingOrder);
actorFlashChromaRed = CreateScreenText("ActorFlashChromaRed", 10.5f, screenTextSortingOrder - 1);
actorFlashChromaCyan = CreateScreenText("ActorFlashChromaCyan", 10.5f, screenTextSortingOrder - 1);
+ truthFlashText = CreateScreenText("ActorTruthFlash", 8.5f, screenTextSortingOrder - 1);
lieClipper = lieText.gameObject.AddComponent();
lieGhostClipper = lieGhostText.gameObject.AddComponent();
systemClipper = systemText.gameObject.AddComponent();
actorFlashClipper = actorFlashText.gameObject.AddComponent();
actorFlashChromaRedClipper = actorFlashChromaRed.gameObject.AddComponent();
actorFlashChromaCyanClipper = actorFlashChromaCyan.gameObject.AddComponent();
+ truthFlashClipper = truthFlashText.gameObject.AddComponent();
lieClipper.Initialize(lieText, expressionScreenMaskRect);
lieGhostClipper.Initialize(lieGhostText, expressionScreenMaskRect);
systemClipper.Initialize(systemText, expressionScreenMaskRect);
actorFlashClipper.Initialize(actorFlashText, expressionScreenMaskRect);
actorFlashChromaRedClipper.Initialize(actorFlashChromaRed, expressionScreenMaskRect);
actorFlashChromaCyanClipper.Initialize(actorFlashChromaCyan, expressionScreenMaskRect);
+ truthFlashClipper.Initialize(truthFlashText, expressionScreenMaskRect);
PositionScreenText();
GameObject scanlineObject = new GameObject("ScreenScanline");
@@ -1702,6 +2156,20 @@ namespace AibisDream.MiniGame.Language
screenDimRenderer.transform.localScale = new Vector3(screen.size.x, screen.size.y, 1f);
screenDimRenderer.enabled = false;
+ GameObject backdropObject = new GameObject("TruthLieFlickerBackdrop");
+ backdropObject.transform.SetParent(runtimeRoot, false);
+ flickerBackdropRenderer = backdropObject.AddComponent();
+ flickerBackdropRenderer.sprite = runtimeWhiteSprite;
+ flickerBackdropRenderer.color = lieBackdropColor;
+ flickerBackdropRenderer.sortingLayerID = GetScreenSortingLayerId();
+ // 底色在记忆/脸之上、屏幕暗脉冲(glow+1)与文字之下,顿挫黑场能盖住它
+ flickerBackdropRenderer.sortingOrder = screenGlowSortingOrder;
+ flickerBackdropRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
+ flickerBackdropRenderer.transform.position = screen.center;
+ flickerBackdropRenderer.transform.localScale = new Vector3(screen.size.x, screen.size.y, 1f);
+ flickerBackdropRenderer.enabled = false;
+ backdropObject.SetActive(false);
+
runtimeObjectsReady = true;
}
@@ -1726,7 +2194,9 @@ namespace AibisDream.MiniGame.Language
TextMeshPro text = obj.AddComponent();
text.font = screenFont != null ? screenFont : TMP_Settings.defaultFontAsset;
text.fontSize = fontSize;
+ text.fontStyle = FontStyles.Normal;
text.enableAutoSizing = false;
+ text.enableWordWrapping = false;
text.alignment = TextAlignmentOptions.Center;
text.overflowMode = TextOverflowModes.Overflow;
text.sortingLayerID = GetScreenSortingLayerId();
@@ -1764,6 +2234,7 @@ namespace AibisDream.MiniGame.Language
PositionText(actorFlashText, center, screen.size);
PositionText(actorFlashChromaRed, center, screen.size);
PositionText(actorFlashChromaCyan, center, screen.size);
+ PositionText(truthFlashText, center, screen.size);
Vector3 systemPosition = center + Vector3.up * screen.extents.y * 0.70f;
PositionText(systemText, systemPosition, new Vector2(screen.size.x, screen.size.y * 0.22f));
}
@@ -1776,6 +2247,7 @@ namespace AibisDream.MiniGame.Language
actorFlashClipper?.Initialize(actorFlashText, expressionScreenMaskRect);
actorFlashChromaRedClipper?.Initialize(actorFlashChromaRed, expressionScreenMaskRect);
actorFlashChromaCyanClipper?.Initialize(actorFlashChromaCyan, expressionScreenMaskRect);
+ truthFlashClipper?.Initialize(truthFlashText, expressionScreenMaskRect);
foreach (TMPRectClipper clipper in actorScrollClippers)
clipper?.Initialize(clipper.GetComponent(), expressionScreenMaskRect);
}
@@ -1788,6 +2260,7 @@ namespace AibisDream.MiniGame.Language
actorFlashClipper?.ReleaseMaterial();
actorFlashChromaRedClipper?.ReleaseMaterial();
actorFlashChromaCyanClipper?.ReleaseMaterial();
+ truthFlashClipper?.ReleaseMaterial();
foreach (TMPRectClipper clipper in actorScrollClippers)
clipper?.ReleaseMaterial();
}
@@ -1881,6 +2354,8 @@ namespace AibisDream.MiniGame.Language
block.SetFloat(ContrastId, contrast);
block.SetFloat(VignetteId, vignette);
block.SetFloat(NoiseId, noise);
+ // 模拟感随记忆自身透明度浮现/退场,避免淡入期间闪线突兀。
+ block.SetFloat(SimGlitchId, simGlitch * opacity);
block.SetFloat(HorizontalTearId, horizontalTear);
block.SetFloat(MemoryImpactId, memoryImpact);
block.SetFloat(MemoryDamageId, memoryDamage);
@@ -1895,17 +2370,22 @@ namespace AibisDream.MiniGame.Language
renderer.SetPropertyBlock(block);
}
- private void TweenMemoryOpacity(SpriteRenderer renderer, float target, float duration)
+ private void TweenMemoryOpacity(
+ SpriteRenderer renderer,
+ float target,
+ float duration,
+ Ease ease = Ease.OutQuad)
{
if (renderer == null)
return;
+ DOTween.Kill(renderer);
DOTween.To(
() => GetMemoryOpacity(renderer),
value => SetMemoryOpacity(renderer, value),
Mathf.Clamp01(target),
Mathf.Max(0.01f, duration))
- .SetEase(Ease.OutQuad)
- .SetTarget(this);
+ .SetEase(ease)
+ .SetTarget(renderer);
}
private float GetMemoryOpacity(SpriteRenderer renderer)
@@ -1926,6 +2406,7 @@ namespace AibisDream.MiniGame.Language
{
if (renderer == null)
return;
+ DOTween.Kill(renderer);
renderer.enabled = false;
renderer.gameObject.SetActive(false);
renderer.sprite = null;
@@ -2070,7 +2551,9 @@ namespace AibisDream.MiniGame.Language
private void HideActorFlashImmediate()
{
+ StopActorFlashLoopImmediate(false);
StopActorOverdriveImmediate();
+ StopTruthLieFlicker();
actorFlashActive = false;
actorFlashCount = 0;
memoryImpact = 0f;
@@ -2091,6 +2574,8 @@ namespace AibisDream.MiniGame.Language
HideTextImmediate(actorFlashText);
HideTextImmediate(actorFlashChromaRed);
HideTextImmediate(actorFlashChromaCyan);
+ truthFlashActive = false;
+ HideTextImmediate(truthFlashText);
}
private static void HideTextImmediate(TextMeshPro text)
@@ -2193,6 +2678,16 @@ namespace AibisDream.MiniGame.Language
}
}
+ private static Preset GetPresetForMemory(MemoryKind kind)
+ {
+ return kind switch
+ {
+ MemoryKind.Sunset => MediumPreset,
+ MemoryKind.PrivateOffice => HeavyPreset,
+ _ => LightPreset
+ };
+ }
+
private static string GetLeakFragment(MemoryKind kind)
{
return kind switch
diff --git a/Assets/Shader/HuoshanMemory.shader b/Assets/Shader/HuoshanMemory.shader
index 5eac53c64..38a4b4d4c 100644
--- a/Assets/Shader/HuoshanMemory.shader
+++ b/Assets/Shader/HuoshanMemory.shader
@@ -9,6 +9,7 @@ Shader "AibisDream/HuoshanMemory"
_Contrast("Contrast", Range(0, 2)) = 1
_Vignette("Vignette", Range(0, 1)) = 0
_Noise("Noise", Range(0, 1)) = 0
+ _SimGlitch("Simulation Glitch", Range(0, 1)) = 0
_HorizontalTear("Horizontal Tear", Range(0, 1)) = 0
_Impact("Transient Memory Impact", Range(0, 1)) = 0
_Damage("Accumulated Memory Damage", Range(0, 1)) = 0
@@ -74,6 +75,7 @@ Shader "AibisDream/HuoshanMemory"
float _Contrast;
float _Vignette;
float _Noise;
+ float _SimGlitch;
float _HorizontalTear;
float _Impact;
float _Damage;
@@ -114,6 +116,9 @@ Shader "AibisDream/HuoshanMemory"
calmMask *= step(0.001, _CalmRadius);
float effect = saturate(_EffectStrength) * (1.0 - calmMask);
+ // Small persistent simulation disturbance, independent of the impact path.
+ float sim = saturate(_SimGlitch) * (1.0 - calmMask * 0.85);
+
float impact = saturate(_Impact);
float damage = saturate(_Damage);
float overdrive = saturate(_Overdrive);
@@ -142,6 +147,17 @@ Shader "AibisDream/HuoshanMemory"
impact * 0.060 +
overdrive * 0.082;
sampleUV.x += tearDirection * tearGate * tearAmount * distortion;
+
+ // Subtle continuous UV wobble plus an occasional narrow-row slip.
+ sampleUV.x += sin((input.uv.y * 18.0 + _Time.y * 0.8) * 6.28318) * 0.0012 * sim;
+ sampleUV.y += sin((input.uv.x * 13.0 - _Time.y * 0.55) * 6.28318) * 0.0007 * sim;
+ float simSlot = floor(_Time.y * 0.9 + _TearSeed * 7.0);
+ float simGate = step(0.62, hash21(float2(simSlot, 3.7)));
+ float simRow = floor(input.uv.y * 34.0);
+ float simRowPick = step(0.90, hash21(float2(simRow + simSlot * 13.0, 11.3)));
+ float simJitterDir = hash21(float2(simSlot * 1.37, simRow)) * 2.0 - 1.0;
+ sampleUV.x += simJitterDir * simGate * simRowPick * 0.0035 * sim;
+
sampleUV.y = (sampleUV.y - 0.5) *
(1.0 + impact * 0.016 + overdrive * 0.025) + 0.5;
sampleUV = saturate(sampleUV);
@@ -194,7 +210,9 @@ Shader "AibisDream/HuoshanMemory"
hash21(float2(dropoutRow * 3.17, timeStep)));
graded *= lerp(1.0, dropoutLevel, dropoutGate * overdrive * 0.72);
- float3 finalColor = lerp(original.rgb, graded, distortion);
+ // The simulation path only opens a light amount of grading/distortion.
+ float3 finalColor = lerp(original.rgb, graded, max(distortion, sim * 0.45));
+
return half4(
finalColor * input.color.rgb,
original.a * input.color.a * _Opacity);