From 84f5cf2346a9bd39a3599bbe4e3354e747d94097 Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Thu, 23 Jul 2026 23:48:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(huoshan):=20=E6=B7=BB=E5=8A=A0=E7=9C=9F?= =?UTF-8?q?=E8=AF=9D=E8=81=9A=E5=90=88=E4=B8=8E=E7=9C=9F=E5=81=87=E9=97=AA?= =?UTF-8?q?=E5=88=87=E6=BC=94=E5=87=BA=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../HuoShan/Language/ExpressionManager.cs | 8 +- .../Language/LanguageParticleManager.cs | 125 +++- .../HuoShan/Language/LanguageYarnCommand.cs | 196 +++++- .../LogReleasePresentationController.cs | 625 ++++++++++++++++-- Assets/Shader/HuoshanMemory.shader | 20 +- 5 files changed, 887 insertions(+), 87 deletions(-) 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);