feat(huoshan): 添加真话聚合与真假闪切演出命令

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-23 23:48:15 +08:00
co-authored by Cursor
parent 377271a5d5
commit 84f5cf2346
5 changed files with 887 additions and 87 deletions
@@ -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)
@@ -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);
}
}
/// <summary>
/// 真话聚合尝试(非等待):FocusHolding 下把目标字拉向最终位置并按序逐字锁定。
/// target 小于 1 时最后几个字永远差一点锁不住,用于"感觉快要成功了"。
/// </summary>
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);
}
/// <summary>真话聚合被打断:目标字向外爆散并恢复乱跳(非等待)。</summary>
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);
}
/// <summary>
/// 梳理完成/真话阶段:目标字改为指定色(默认白),并取消加粗以便大屏辨认。
/// 玩法阶段颜色不受影响;下一次 start_expression 会通过 RestoreTargetPresentationDefaults 还原。
/// </summary>
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;
}
@@ -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));
}
/// <summary>
/// 记忆淡入(非阻塞):透明度在 fadeDuration 内显现,画面同时按 pushSpeed 持续推近;
/// targetOpacity 是本次淡入的目标透明度。horizontalSpeed 为旧 Yarn 兼容参数,基础层不再横移。
/// 争夺段的 jolt / tear / impact 仍会叠加。
/// </summary>
[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));
}
/// <summary>
/// 随对白把当前记忆透明度推进到目标值(非阻塞)。
/// <<expression_memory_alpha 0.65 0.45>>
/// </summary>
[YarnCommand("expression_memory_alpha")]
public static void ExpressionMemoryAlpha(float target, float duration = 0.4f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SetActiveMemoryOpacity(target, duration);
}
/// <summary>
@@ -315,6 +331,85 @@ namespace AibisDream.MiniGame.Language
presentation.PlayMemoryJolt(offset, duration, tear);
}
/// <summary>
/// 调节记忆的轻微失真扰动(非阻塞)。默认强度由演出预设决定,本命令用于演出中微调。
/// <<expression_memory_sim 0.55 0.3>>
/// </summary>
[YarnCommand("expression_memory_sim")]
public static void ExpressionMemorySim(float target, float duration = 0.3f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.SetSimulationGlitch(target, duration);
}
/// <summary>
/// 真话聚合尝试(非阻塞):目标字被慢慢拉向最终位置并按字序逐个锁定成真心话。
/// target 小于 1 时最后几个字永远差一点锁不住——"感觉快要成功了"。
/// <<expression_truth_gather 0.9 3.2>>
/// </summary>
[YarnCommand("expression_truth_gather")]
public static void ExpressionTruthGather(float target = 0.9f, float duration = 3f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.BeginTruthGatherAttempt(target, duration);
}
/// <summary>
/// 真话聚合被打断(非阻塞):目标字向外爆散并恢复乱跳。burst 为世界单位的爆散距离。
/// <<expression_truth_scatter 0.9>>
/// </summary>
[YarnCommand("expression_truth_scatter")]
public static void ExpressionTruthScatter(float burst = 0.9f)
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.ScatterTruthGatherAttempt(burst);
}
/// <summary>
/// 火山脸错误闪红(非阻塞):聚合失败时短暂替换红屏 Sprite,随后恢复,并轻微下沉。
/// <<expression_face_error 0.5 0.85>>
/// </summary>
[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);
}
/// <summary>
/// 结尾真假快速闪切(非阻塞,节奏由同长度 wait 控制):真心话与谎话交替独占屏幕,
/// 两者的屏幕底色(电视头背景)不同,越到后面切得越快,结束定格在真话帧。
/// <<expression_truth_lie_flicker "没问题|冇问题|帽问题" "我就是个笑话" 3.2 0.24 12>>
/// </summary>
[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);
}
/// <summary>
/// 定格的谎话被 glitch 翻转成真话(阻塞):大字乱跳中长度从谎话渐变到真话、真假底色抢闪,
/// 最后落在暖色真话。紧接 expression_memory_cut + expression_truth_snap 收尾。
/// <<expression_lie_morph_truth "没问题" "我就是个笑话" 0.9>>
/// </summary>
[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
/// <summary>
/// 演员大字闪现(阻塞):抖动的粒子字瞬间隐藏,屏幕中央出现白色大字(红青色散双影+急速缩放),
/// 停顿 holdDuration 秒后恢复粒子字抖动。
/// 停顿 holdDuration 秒后始终恢复粒子字抖动;结尾定格请使用 expression_actor_flash_in
/// <<expression_actor_flash "没问题" 0.6 10.5 1.15 1.60 0.03 0.22 0.5 0.2>>
/// </summary>
[YarnCommand("expression_actor_flash")]
@@ -366,19 +461,95 @@ namespace AibisDream.MiniGame.Language
glitchPeak));
}
/// <summary>
/// 演员大字入场并保持(阻塞):入场表现与 expression_actor_flash 相同,但不执行退场,
/// 适合在段落结尾定格。后续清场命令仍会正常移除它。
/// <<expression_actor_flash_in "没问题" 0.35 10.5 1.1 1.45 0.008 0.06 0.5 0>>
/// </summary>
[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));
}
/// <summary>
/// 非阻塞启动“没问题 → 冇问题 → 帽问题”三拍无限闪现循环。
/// <<expression_actor_flash_loop_start>>
/// </summary>
[YarnCommand("expression_actor_flash_loop_start")]
public static void ExpressionActorFlashLoopStart()
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.StartActorFlashLoop();
}
/// <summary>
/// 立即停止无限闪现循环、清除当前大字,并恢复晃动粒子字。
/// <<expression_actor_flash_loop_stop>>
/// </summary>
[YarnCommand("expression_actor_flash_loop_stop")]
public static void ExpressionActorFlashLoopStop()
{
if (TryGetPresentation(out LogReleasePresentationController presentation))
presentation.StopActorFlashLoop();
}
/// <summary>
/// 真话闪现(阻塞):谎话大字之间,暖色完整真话带细微抖动挣扎浮现一拍;
/// "被掐断"由紧随其后的 expression_glitch_pulse 表现。不推进谎话冲击等级。
/// <<expression_actor_truth_flash "我就是个笑话" 0.35 8.5 0.55 0.02>>
/// </summary>
[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));
}
/// <summary>
/// 老虎机换字(非阻塞,节拍由 Yarn wait 控制):聚焦字在 duration 内只从 charPool 里疯狂换字。
/// <<expression_actor_slot "没问题冇" 5 0.05 14>>
/// truthPool 非空时其字符也混入换字池,并以真话暖色显示。
/// <<expression_actor_slot "没问题冇" 5 0.05 14 "我就是个笑话">>
/// </summary>
[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);
}
/// <summary>
@@ -588,4 +759,3 @@ namespace AibisDream.MiniGame.Language
// }
}
}
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -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);