diff --git a/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader b/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader
index a7155efb8..1a8897d7a 100644
--- a/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader
+++ b/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader
@@ -13,6 +13,9 @@ Shader "AibisDream/SpriteNoiseGlitch"
_NoiseScale("Noise Scale (lower = bigger pixel dots)", Range(40, 500)) = 80
_NoiseDensity("Noise Density (white dot probability)", Range(0, 1)) = 0.1
_NoiseBrightness("Noise Brightness", Range(0, 2)) = 1.0
+ _BaseNoiseScale("Ambient Noise Scale (higher = finer dots)", Range(80, 800)) = 320
+ _BaseNoiseDensity("Ambient Noise Density (always-on, 0 = off)", Range(0, 0.3)) = 0
+ _BaseNoiseBrightness("Ambient Noise Brightness", Range(0, 1)) = 0.55
_ScanlineCount("Scanline Count", Float) = 80
_ScanlineStrength("Scanline Strength", Range(0, 1)) = 0.3
@@ -87,6 +90,9 @@ Shader "AibisDream/SpriteNoiseGlitch"
float _NoiseScale;
float _NoiseDensity;
float _NoiseBrightness;
+ float _BaseNoiseScale;
+ float _BaseNoiseDensity;
+ float _BaseNoiseBrightness;
float _ScanlineCount;
float _ScanlineStrength;
float _BlockSize;
@@ -130,7 +136,7 @@ Shader "AibisDream/SpriteNoiseGlitch"
return saturate((v - lo) / (hi - lo));
}
- float applyCalmField(float2 uv, float intensity)
+ float calmFieldFactor(float2 uv)
{
float2 fieldPosition = float2(
(uv.x - _CalmField.x) * _CalmFieldAspect,
@@ -148,7 +154,12 @@ Shader "AibisDream/SpriteNoiseGlitch"
float calmMask = 1.0 - smoothstep(-feather, feather, distanceToField);
float calmBlend = saturate(calmMask * _CalmFieldStrength);
- return intensity * lerp(1.0, _CalmFieldResidual, calmBlend);
+ return lerp(1.0, _CalmFieldResidual, calmBlend);
+ }
+
+ float applyCalmField(float2 uv, float intensity)
+ {
+ return intensity * calmFieldFactor(uv);
}
Varyings vert(Attributes input)
@@ -193,6 +204,12 @@ Shader "AibisDream/SpriteNoiseGlitch"
half3 baseColor = half3(whiteDot, whiteDot, whiteDot);
baseColor *= (1.0 - scanMask * 0.5);
+ // 常驻细密噪点:与 glitch 强度无关,一直存在;受 calm field 抑制保证文字可读
+ float ambientDensity = _BaseNoiseDensity * calmFieldFactor(uv);
+ float ambientNoise = hash31(float3(floor(uv * _BaseNoiseScale), timeSeed));
+ float ambientDot = step(1.0 - ambientDensity, ambientNoise) * _BaseNoiseBrightness;
+ baseColor = max(baseColor, half3(ambientDot, ambientDot, ambientDot));
+
// ============================================================
// STAGE 2: horizontal block displacement + chromatic aberration
// ============================================================
@@ -323,6 +340,9 @@ Shader "AibisDream/SpriteNoiseGlitch"
float _NoiseScale;
float _NoiseDensity;
float _NoiseBrightness;
+ float _BaseNoiseScale;
+ float _BaseNoiseDensity;
+ float _BaseNoiseBrightness;
float _ScanlineCount;
float _ScanlineStrength;
float _BlockSize;
@@ -366,7 +386,7 @@ Shader "AibisDream/SpriteNoiseGlitch"
return saturate((v - lo) / (hi - lo));
}
- float applyCalmField(float2 uv, float intensity)
+ float calmFieldFactor(float2 uv)
{
float2 fieldPosition = float2(
(uv.x - _CalmField.x) * _CalmFieldAspect,
@@ -384,7 +404,12 @@ Shader "AibisDream/SpriteNoiseGlitch"
float calmMask = 1.0 - smoothstep(-feather, feather, distanceToField);
float calmBlend = saturate(calmMask * _CalmFieldStrength);
- return intensity * lerp(1.0, _CalmFieldResidual, calmBlend);
+ return lerp(1.0, _CalmFieldResidual, calmBlend);
+ }
+
+ float applyCalmField(float2 uv, float intensity)
+ {
+ return intensity * calmFieldFactor(uv);
}
Varyings vert(Attributes input)
@@ -422,6 +447,12 @@ Shader "AibisDream/SpriteNoiseGlitch"
half3 baseColor = half3(whiteDot, whiteDot, whiteDot);
baseColor *= (1.0 - scanMask * 0.5);
+ // 常驻细密噪点:与 glitch 强度无关,一直存在;受 calm field 抑制保证文字可读
+ float ambientDensity = _BaseNoiseDensity * calmFieldFactor(uv);
+ float ambientNoise = hash31(float3(floor(uv * _BaseNoiseScale), timeSeed));
+ float ambientDot = step(1.0 - ambientDensity, ambientNoise) * _BaseNoiseBrightness;
+ baseColor = max(baseColor, half3(ambientDot, ambientDot, ambientDot));
+
float blockRow = floor(uv.y * _BlockSize);
float blockTimeSeed = floor(t * 6.0);
float blockRand = hash21(float2(blockRow, blockTimeSeed));
diff --git a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
index 72908c474..370b9f1bc 100644
--- a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
+++ b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
@@ -18,6 +18,11 @@ public class SpriteNoiseGlitchController : MonoBehaviour
[SerializeField] private AudioClip _stage3Clip;
[SerializeField, Range(0f, 1f)] private float _audioVolume = 0.5f;
+ [Header("Ambient Fine Noise(常驻细密噪点,密度 0 = 关闭)")]
+ [SerializeField, Range(0f, 0.3f)] private float _ambientNoiseDensity = 0f;
+ [SerializeField, Range(80f, 800f)] private float _ambientNoiseScale = 320f;
+ [SerializeField, Range(0f, 1f)] private float _ambientNoiseBrightness = 0.55f;
+
[Header("Text Calm Field")]
[SerializeField] private Vector2 _calmFieldPadding = new Vector2(0.55f, 0.4f);
[SerializeField, Range(0.05f, 1f)] private float _calmFieldFeather = 0.45f;
@@ -36,6 +41,9 @@ public class SpriteNoiseGlitchController : MonoBehaviour
private bool _calmFieldRequested;
private static readonly int PropGlitchIntensity = Shader.PropertyToID("_GlitchIntensity");
+ private static readonly int PropBaseNoiseDensity = Shader.PropertyToID("_BaseNoiseDensity");
+ private static readonly int PropBaseNoiseScale = Shader.PropertyToID("_BaseNoiseScale");
+ private static readonly int PropBaseNoiseBrightness = Shader.PropertyToID("_BaseNoiseBrightness");
private static readonly int PropCalmField = Shader.PropertyToID("_CalmField");
private static readonly int PropCalmFieldAspect = Shader.PropertyToID("_CalmFieldAspect");
private static readonly int PropCalmFieldStrength = Shader.PropertyToID("_CalmFieldStrength");
@@ -71,6 +79,30 @@ public class SpriteNoiseGlitchController : MonoBehaviour
{
ApplyIntensity();
ApplyCalmField();
+ ApplyAmbientNoise();
+ }
+
+ ///
+ /// 设置常驻细密噪点(与 glitch 强度无关,一直显示)。density 为 0 时关闭。
+ ///
+ public void SetAmbientNoise(float density, float scale, float brightness)
+ {
+ _ambientNoiseDensity = Mathf.Clamp(density, 0f, 0.3f);
+ _ambientNoiseScale = Mathf.Max(1f, scale);
+ _ambientNoiseBrightness = Mathf.Clamp01(brightness);
+ ApplyAmbientNoise();
+ }
+
+ private void ApplyAmbientNoise()
+ {
+ EnsureRuntimeObjects();
+ if (_renderer == null) return;
+
+ _renderer.GetPropertyBlock(_mpb);
+ _mpb.SetFloat(PropBaseNoiseDensity, _ambientNoiseDensity);
+ _mpb.SetFloat(PropBaseNoiseScale, _ambientNoiseScale);
+ _mpb.SetFloat(PropBaseNoiseBrightness, _ambientNoiseBrightness);
+ _renderer.SetPropertyBlock(_mpb);
}
private void OnDisable()
@@ -343,6 +375,7 @@ public class SpriteNoiseGlitchController : MonoBehaviour
ApplyIntensity();
ApplyCalmField();
+ ApplyAmbientNoise();
}
#endif
}
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
index d5eb8b230..a237a4e72 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
@@ -32,6 +32,19 @@ namespace AibisDream
[Tooltip("噪波故障效果控制器,可选;置于 Expression View 子物体上")]
[SerializeField] private SpriteNoiseGlitchController glitchController;
+ [Header("常驻细密噪点(不随 glitch 强度消失,营造老屏幕底噪)")]
+ [Tooltip("常驻噪点密度,0 = 关闭(恢复旧行为:LOG1 无噪点)")]
+ [SerializeField, Range(0f, 0.3f)] private float ambientNoiseDensity = 0.06f;
+ [Tooltip("噪点细密程度,越大颗粒越细(glitch 主噪点默认 80)")]
+ [SerializeField, Range(80f, 800f)] private float ambientNoiseScale = 360f;
+ [Tooltip("常驻噪点亮度(低于 glitch 主噪点,保持底噪感)")]
+ [SerializeField, Range(0f, 1f)] private float ambientNoiseBrightness = 0.5f;
+
+ // 游玩期干扰联动的 glitch 基线;yarn glitch_transition 调用后暂停联动直到下一轮
+ private float gameplayGlitchBaseline;
+ private bool yarnGlitchOverride;
+ private Coroutine glitchPulseRoutine;
+
void Start()
{
// 注册到系统字典
@@ -58,6 +71,9 @@ namespace AibisDream
{
glitchController = GetComponentInChildren(true);
}
+
+ // 释放log屏幕常驻细密噪点(shader 默认关闭,不影响其他使用该 shader 的场景)
+ glitchController?.SetAmbientNoise(ambientNoiseDensity, ambientNoiseScale, ambientNoiseBrightness);
}
private void LateUpdate()
@@ -102,27 +118,32 @@ namespace AibisDream
/// 目标句子
/// 完成时触发的对话节点名称(可选)
/// 非目标候选粒子总数;<0 时使用 Inspector 默认候选池大小
- public void StartSystem(List anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1)
+ /// 干扰表现档位索引;<0 时使用粒子管理器的全局干扰参数
+ public void StartSystem(List anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1, int interferencePresetIndex = -1)
{
glitchController?.ClearCalmField(true);
+ // 新一轮重置干扰联动(解除 yarn 覆盖、基线归零)
+ yarnGlitchOverride = false;
+ gameplayGlitchBaseline = 0f;
+
if (particleManager != null)
{
// 使用传入的配置,如果没有则使用默认配置
var phrases = anxietyPhrases ?? defaultAnxietyPhrases;
var sentence = targetSentence ?? defaultTargetSentence;
-
- particleManager.InitializeSystem(phrases, sentence, completionNodeName, nonTargetParticleTotal);
+
+ particleManager.InitializeSystem(phrases, sentence, completionNodeName, nonTargetParticleTotal, interferencePresetIndex);
}
}
///
/// 启动粒子系统(字符串数组版本,方便 Yarn 调用)
///
- public void StartSystem(string[] anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1)
+ public void StartSystem(string[] anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1, int interferencePresetIndex = -1)
{
List phrasesList = anxietyPhrases != null ? new List(anxietyPhrases) : null;
- StartSystem(phrasesList, targetSentence, completionNodeName, nonTargetParticleTotal);
+ StartSystem(phrasesList, targetSentence, completionNodeName, nonTargetParticleTotal, interferencePresetIndex);
}
///
@@ -233,21 +254,68 @@ namespace AibisDream
spriteRenderer.gameObject.SetActive(false);
}
+ ///
+ /// 游玩期干扰联动:平滑过渡到基线强度。yarn 手动驱动过 glitch 后(override)忽略,
+ /// 直到下一次 StartSystem 重置。
+ ///
+ public void SetGameplayGlitchBaseline(float target, float smoothDuration = 0.5f)
+ {
+ if (glitchController == null || yarnGlitchOverride) return;
+ gameplayGlitchBaseline = Mathf.Clamp01(target);
+ if (glitchPulseRoutine != null) return; // 脉冲进行中,结束后会回落到新基线
+ glitchController.TransitionTo(gameplayGlitchBaseline, smoothDuration);
+ }
+
+ ///
+ /// Glitch 短脉冲:快速升到 peak 再回落到游玩基线(用于完成瞬间等强调时刻)。
+ ///
+ public void PlayGlitchPulse(float peak, float duration = 0.5f)
+ {
+ if (glitchController == null || yarnGlitchOverride) return;
+ if (glitchPulseRoutine != null)
+ {
+ StopCoroutine(glitchPulseRoutine);
+ }
+ glitchPulseRoutine = StartCoroutine(GlitchPulseRoutine(Mathf.Clamp01(peak), duration));
+ }
+
+ private IEnumerator GlitchPulseRoutine(float peak, float duration)
+ {
+ yield return glitchController.StartCoroutine(
+ glitchController.TransitionToAndWait(peak, duration * 0.35f));
+ yield return glitchController.StartCoroutine(
+ glitchController.TransitionToAndWait(gameplayGlitchBaseline, duration * 0.65f));
+ glitchPulseRoutine = null;
+ }
+
///
/// Glitch 噪波过渡:从当前强度平滑过渡到 to,不瞬移(类似 DOTween)。
+ /// yarn 手动驱动后,游玩期干扰联动暂停(避免互相打架)。
///
public void TransitionGlitch(float to, float duration = 0f)
{
if (glitchController == null) return;
+ MarkYarnGlitchOverride();
glitchController.TransitionTo(to, duration);
}
+ private void MarkYarnGlitchOverride()
+ {
+ yarnGlitchOverride = true;
+ if (glitchPulseRoutine != null)
+ {
+ StopCoroutine(glitchPulseRoutine);
+ glitchPulseRoutine = null;
+ }
+ }
+
///
/// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to。
///
public IEnumerator TransitionGlitchAndWait(float to, float duration = 0f)
{
if (glitchController == null) yield break;
+ MarkYarnGlitchOverride();
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
@@ -257,6 +325,7 @@ namespace AibisDream
public IEnumerator TransitionGlitchAndWait(float from, float to, float duration)
{
if (glitchController == null) yield break;
+ MarkYarnGlitchOverride();
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
index a6b9642c3..3eebc274a 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs
@@ -96,6 +96,36 @@ namespace AibisDream.MiniGame.Language
[SerializeField, Range(0.1f, 1f)] private float nonTargetChangeInterval = 0.4f; // 非目标粒子变字间隔(越小越快)
[SerializeField, Range(0f, 0.03f)] private float nonTargetFloatAmplitude = 0.015f; // 非目标粒子漂浮幅度
+ ///
+ /// 按轮干扰表现档位:递进主要靠"文字更躁"传达,不实质加难
+ ///
+ [System.Serializable]
+ public class InterferencePresentationPreset
+ {
+ [Tooltip("非目标粒子抖动强度")] public float shakeIntensity = 0.02f;
+ [Tooltip("非目标粒子抖动速度")] public float shakeSpeed = 8f;
+ [Tooltip("非目标粒子变字间隔(越小越快越躁)")] public float changeInterval = 0.4f;
+ [Tooltip("非目标粒子漂浮幅度")] public float floatAmplitude = 0.015f;
+ [Tooltip("干扰计数映射的 glitch 基线上限(0 关闭联动)")] [Range(0f, 0.3f)] public float glitchBaselineMax = 0f;
+ [Tooltip("非目标粒子向目标簇的轻微漂移加速度(仅观感,远小于玩家施力,0 关闭)")] public float driftBias = 0f;
+ }
+
+ [Header("干扰表现递进(start_expression 第5参为档位索引,缺省用上方全局参数)")]
+ [SerializeField] private InterferencePresentationPreset[] interferencePresets = new InterferencePresentationPreset[]
+ {
+ new InterferencePresentationPreset(),
+ new InterferencePresentationPreset
+ {
+ shakeIntensity = 0.035f, shakeSpeed = 10f, changeInterval = 0.28f,
+ floatAmplitude = 0.02f, glitchBaselineMax = 0.08f, driftBias = 0.25f
+ },
+ new InterferencePresentationPreset
+ {
+ shakeIntensity = 0.05f, shakeSpeed = 12f, changeInterval = 0.18f,
+ floatAmplitude = 0.028f, glitchBaselineMax = 0.15f, driftBias = 0.4f
+ },
+ };
+
[Header("连接线粗细")]
[SerializeField] private float targetConnectionThickness = 0.04f; // 目标连线粗细
[SerializeField] private float nonTargetConnectionThickness = 0.02f; // 非目标连线粗细
@@ -135,17 +165,42 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private string expressionConfirmTimelineName = "火山表达模块/火山表达确认";
[SerializeField] private float focusZoomScale = 1.6f;
[SerializeField] private float focusMoveDuration = 1.2f;
- [SerializeField] private float focusFadeDuration = 1f;
+ [Tooltip("聚焦阶段 UI/粒子淡出时长 = focusMoveDuration × 此比例,与粒子聚焦移动同节奏")]
+ [SerializeField, Range(0.3f, 1f)] private float focusFadeRatio = 0.8f;
[Tooltip("再次 start_expression 时,若上一轮粒子/UI 仍在,先淡出再重开;协程 duration≤0 时用此值(秒)")]
[SerializeField] private float restartExpressionFadeDuration = 0.55f;
[SerializeField] private float probabilityDuration = 3.5f;
[SerializeField] private float predictionSequenceGap = 0.4f;
[SerializeField] private float spreadDistance = 0.35f;
+ [Header("完成释放表现")]
+ [Tooltip("完成瞬间冲击波扩散时长(秒)")]
+ [SerializeField] private float completionShockwaveDuration = 0.6f;
+ [Tooltip("非目标粒子被吹散的径向冲量")]
+ [SerializeField] private float completionBlastForce = 3.5f;
+ [Tooltip("吹散后非目标粒子透明度(相对 baseAlpha 的比例)")]
+ [SerializeField, Range(0f, 1f)] private float completionBlastAlphaRatio = 0.35f;
+ [Tooltip("吹散后到进入平静态的延迟(秒)")]
+ [SerializeField] private float completionCalmDelay = 0.55f;
+ [Tooltip("完成瞬间 glitch 脉冲峰值(0 关闭)")]
+ [SerializeField, Range(0f, 1f)] private float completionGlitchPulsePeak = 0.25f;
+
+ [Header("节奏统一")]
+ [Tooltip("完成时确认按钮淡入/上浮时长(秒)")]
+ [SerializeField] private float confirmButtonFadeDuration = 0.35f;
+ [Tooltip("确认按钮上浮距离(Canvas 局部单位)")]
+ [SerializeField] private float confirmButtonRiseOffset = 0.35f;
+ [Tooltip("入场 cascade 进行到该比例时并行启动火山表达panel(消除串行空档)")]
+ [SerializeField, Range(0f, 1f)] private float panelStartAtEntranceProgress = 0.6f;
+ [Tooltip("状态文字数值滚动速度(百分点/秒)")]
+ [SerializeField] private float statusCountUpSpeed = 90f;
+
[Header("聚焦表现")]
[SerializeField] private Color focusRingColor = Color.white;
[SerializeField, Range(0.01f, 0.3f)] private float focusRingThickness = 0.08f;
[SerializeField, Range(0.05f, 1f)] private float focusRingAlpha = 0.85f;
+ [Tooltip("不稳定期聚焦环虚线旋转速度(DashOffset/秒)")]
+ [SerializeField] private float focusRingDashRotateSpeed = 0.35f;
[SerializeField] private float statusFadeOutDuration = 0.45f;
[SerializeField] private float shuffleDistance = 0.35f;
[SerializeField] private float shuffleFrequency = 3.5f;
@@ -186,7 +241,6 @@ namespace AibisDream.MiniGame.Language
// 效果系统(传播效果已移除,保留空列表供渲染器使用)
private static readonly List s_emptyPropagations = new List();
- private Dictionary previousConnections = new Dictionary();
private bool initializationComplete = false;
private int initializationFrames = 0;
@@ -220,6 +274,7 @@ namespace AibisDream.MiniGame.Language
public GameObject ringObject;
public Disc ringDisc;
public TMP_Text probabilityLabel;
+ public Vector3 labelBasePos;
public float probability = 90f;
public float targetProbability = 100f;
public float startShakeIntensity;
@@ -230,6 +285,50 @@ namespace AibisDream.MiniGame.Language
public int orderIndex;
}
+ /// 聚焦阶段 UI/粒子统一淡出时长(与聚焦移动同节奏)
+ private float FocusUiFadeDuration => focusMoveDuration * focusFadeRatio;
+
+ // 入场表现是否结束(panel timeline 并行播放时,两者都完成才 allowInput)
+ private bool entranceVisualsFinished = true;
+
+ // 确认按钮基准位置(淡入上浮动画的落点,首次完成时捕获)
+ private Vector2 confirmButtonBaseAnchoredPos;
+ private bool confirmButtonBasePosCaptured;
+
+ // 状态文字 count-up 显示值与脉冲检测
+ private float displayedIntegrationPercent;
+ private float displayedInterferenceCount;
+ private int lastInterferenceCount = -1;
+ private Color interferenceTextBaseColor = Color.white;
+ private bool interferenceTextBaseColorCaptured;
+
+ // 完成瞬间吹散→延迟平静协程
+ private Coroutine completionBurstRoutine;
+
+ // 当前生效的干扰表现参数(InitializeSystem 时由档位或全局参数决定)
+ private float activeShakeIntensity;
+ private float activeShakeSpeed;
+ private float activeChangeInterval;
+ private float activeFloatAmplitude;
+ private float activeGlitchBaselineMax;
+ private float activeDriftBias;
+ private bool interferenceActiveParamsSet;
+ private float glitchLinkTimer;
+
+ // 顶层表达管理器(glitch 脉冲/联动用),粒子管理器挂在其子物体上
+ private ExpressionManager expressionManagerRef;
+ private ExpressionManager ExpressionManagerRef
+ {
+ get
+ {
+ if (expressionManagerRef == null)
+ {
+ expressionManagerRef = GetComponentInParent(true);
+ }
+ return expressionManagerRef;
+ }
+ }
+
private bool focusTweensCompleted = false;
// 边界
@@ -250,6 +349,9 @@ namespace AibisDream.MiniGame.Language
// 是否已经初始化
private bool isInitialized = false;
+ // 鼠标坐标换算用主相机缓存(避免每帧 Camera.main 查找)
+ private Camera cachedMainCamera;
+
/// Inspector 中的候选粒子总数,用于未在 Yarn 中指定非目标数量时恢复默认
private int defaultCandidateCountFromInspector;
private bool hasCapturedDefaultCandidateCount;
@@ -261,6 +363,43 @@ namespace AibisDream.MiniGame.Language
hasCapturedDefaultCandidateCount = true;
}
+ ///
+ /// 选择本轮生效的干扰表现参数:有效档位用预设,否则回落到 Inspector 全局参数(行为与旧版一致)
+ ///
+ private void ApplyInterferencePreset(int presetIndex)
+ {
+ if (presetIndex >= 0 && interferencePresets != null &&
+ presetIndex < interferencePresets.Length && interferencePresets[presetIndex] != null)
+ {
+ var p = interferencePresets[presetIndex];
+ activeShakeIntensity = p.shakeIntensity;
+ activeShakeSpeed = p.shakeSpeed;
+ activeChangeInterval = p.changeInterval;
+ activeFloatAmplitude = p.floatAmplitude;
+ activeGlitchBaselineMax = p.glitchBaselineMax;
+ activeDriftBias = p.driftBias;
+ }
+ else
+ {
+ activeShakeIntensity = nonTargetShakeIntensity;
+ activeShakeSpeed = nonTargetShakeSpeed;
+ activeChangeInterval = nonTargetChangeInterval;
+ activeFloatAmplitude = nonTargetFloatAmplitude;
+ activeGlitchBaselineMax = 0f;
+ activeDriftBias = 0f;
+ }
+ interferenceActiveParamsSet = true;
+ glitchLinkTimer = 0f;
+ }
+
+ private void EnsureActiveInterferenceParams()
+ {
+ if (!interferenceActiveParamsSet)
+ {
+ ApplyInterferencePreset(-1);
+ }
+ }
+
private void Start()
{
// Start 中不再自动初始化,等待外部调用 InitializeSystem
@@ -275,9 +414,11 @@ namespace AibisDream.MiniGame.Language
///
/// 非目标候选粒子总数(蓝/干扰侧可交互粒子数量)。≥0 时候选池大小 = 目标句字数 + 该值;<0 时使用 Inspector 的 Candidate Count。
///
- public void InitializeSystem(List phrases, string sentence, string dialogNode = null, int nonTargetParticleTotal = -1)
+ /// 干扰表现档位索引;<0 或越界时使用 Inspector 全局干扰参数
+ public void InitializeSystem(List phrases, string sentence, string dialogNode = null, int nonTargetParticleTotal = -1, int interferencePresetIndex = -1)
{
CaptureDefaultCandidateCountFromInspector();
+ ApplyInterferencePreset(interferencePresetIndex);
string useSentence = string.IsNullOrEmpty(sentence) ? "这是一个测试示例" : sentence;
@@ -304,6 +445,8 @@ namespace AibisDream.MiniGame.Language
if (interferenceCountText != null)
{
interferenceTextBaseAlpha = interferenceCountText.color.a;
+ interferenceTextBaseColor = interferenceCountText.color;
+ interferenceTextBaseColorCaptured = true;
}
if (focusSequenceButton != null)
@@ -370,6 +513,11 @@ namespace AibisDream.MiniGame.Language
StopCoroutine(confirmTimelineRoutine);
confirmTimelineRoutine = null;
}
+ if (completionBurstRoutine != null)
+ {
+ StopCoroutine(completionBurstRoutine);
+ completionBurstRoutine = null;
+ }
DOTween.Kill(this);
foreach (var p in candidateParticles)
@@ -495,7 +643,13 @@ namespace AibisDream.MiniGame.Language
StopCoroutine(expressionPanelRoutine);
expressionPanelRoutine = null;
}
-
+
+ if (completionBurstRoutine != null)
+ {
+ StopCoroutine(completionBurstRoutine);
+ completionBurstRoutine = null;
+ }
+
// 停止所有 DOTween 动画
DOTween.Kill(this);
foreach (var particle in candidateParticles)
@@ -633,7 +787,8 @@ namespace AibisDream.MiniGame.Language
particle.SetFont(chineseFontAsset);
particle.SetColors(targetParticleColor, nonTargetParticleColor, nonTargetParticleColor, targetCalmColor);
particle.SetFontStyles(targetParticleFontSize, nonTargetParticleFontSize, targetParticleBold, nonTargetParticleBold);
- particle.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude);
+ EnsureActiveInterferenceParams();
+ particle.SetInterferenceParams(activeShakeIntensity, activeShakeSpeed, activeChangeInterval, activeFloatAmplitude);
SetParticleBounds(particle);
if (enableBloomGlow)
@@ -808,11 +963,6 @@ namespace AibisDream.MiniGame.Language
{
initializationComplete = true;
}
- else if (initializationFrames >= 20)
- {
- // 建立初始连接状态
- BuildConnectionMap();
- }
}
HandleDebugShortcuts();
UpdateConnections();
@@ -823,11 +973,6 @@ namespace AibisDream.MiniGame.Language
if (completionPhase == CompletionPhase.None)
{
- if (initializationComplete && !interferenceLogicSuspended)
- {
- DetectConnectionChanges();
- }
-
if (!isCompleted && CheckRedParticlesConnected())
{
isCompleted = true;
@@ -842,6 +987,8 @@ namespace AibisDream.MiniGame.Language
if (!interferenceLogicSuspended)
{
ApplySeparationForces();
+ ApplyInterferenceDrift();
+ UpdateInterferenceGlitchLink();
}
}
@@ -968,6 +1115,29 @@ namespace AibisDream.MiniGame.Language
particle.connections.Clear();
}
+ // 聚焦及之后阶段非目标粒子已淡出,只需维护目标粒子间连线,跳过全量 O(n²)
+ if (completionPhase == CompletionPhase.Focusing ||
+ completionPhase == CompletionPhase.FocusHolding ||
+ completionPhase == CompletionPhase.Finished)
+ {
+ for (int i = 0; i < targetParticles.Count; i++)
+ {
+ for (int j = i + 1; j < targetParticles.Count; j++)
+ {
+ float d = Vector3.Distance(
+ targetParticles[i].transform.position,
+ targetParticles[j].transform.position
+ );
+ if (d < connectionDistance)
+ {
+ targetParticles[i].connections.Add(targetParticles[j]);
+ targetParticles[j].connections.Add(targetParticles[i]);
+ }
+ }
+ }
+ return;
+ }
+
// 完成目标后,不再创建目标粒子与非目标粒子的新连接
bool suppressTargetNonTargetConnections = isCompleted;
@@ -998,51 +1168,6 @@ namespace AibisDream.MiniGame.Language
}
}
- private void BuildConnectionMap()
- {
- previousConnections.Clear();
- for (int i = 0; i < candidateParticles.Count; i++)
- {
- for (int j = i + 1; j < candidateParticles.Count; j++)
- {
- float distance = Vector3.Distance(
- candidateParticles[i].transform.position,
- candidateParticles[j].transform.position
- );
-
- if (distance < connectionDistance)
- {
- string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}";
- previousConnections[key] = true;
- }
- }
- }
- }
-
- private void DetectConnectionChanges()
- {
- Dictionary currentConnections = new Dictionary();
-
- for (int i = 0; i < candidateParticles.Count; i++)
- {
- for (int j = i + 1; j < candidateParticles.Count; j++)
- {
- float distance = Vector3.Distance(
- candidateParticles[i].transform.position,
- candidateParticles[j].transform.position
- );
-
- if (distance < connectionDistance)
- {
- string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}";
- currentConnections[key] = true;
- }
- }
- }
-
- previousConnections = currentConnections;
- }
-
private bool CheckRedParticlesConnected()
{
if (targetParticles.Count == 0) return false;
@@ -1138,13 +1263,31 @@ namespace AibisDream.MiniGame.Language
}
}
- // 首先激活 button 但不可交互,播放火山表达确认后再解锁(若之前被 fade 过,需重置 alpha)
+ // 激活 button 并淡入上浮(不可交互,播放火山表达确认后再解锁),与确认 timeline 并行
if (focusSequenceButton != null)
{
var btnCg = focusSequenceButton.GetComponent();
- if (btnCg != null) btnCg.alpha = 1f;
+ if (btnCg == null)
+ btnCg = focusSequenceButton.gameObject.AddComponent();
+ var btnRect = focusSequenceButton.transform as RectTransform;
+ if (btnRect != null && !confirmButtonBasePosCaptured)
+ {
+ confirmButtonBaseAnchoredPos = btnRect.anchoredPosition;
+ confirmButtonBasePosCaptured = true;
+ }
+
focusSequenceButton.gameObject.SetActive(true);
focusSequenceButton.interactable = false;
+
+ DOTween.Kill(btnCg);
+ btnCg.alpha = 0f;
+ btnCg.DOFade(1f, confirmButtonFadeDuration).SetEase(Ease.OutQuad);
+ if (btnRect != null)
+ {
+ btnRect.DOKill();
+ btnRect.anchoredPosition = confirmButtonBaseAnchoredPos + Vector2.down * confirmButtonRiseOffset;
+ btnRect.DOAnchorPos(confirmButtonBaseAnchoredPos, confirmButtonFadeDuration).SetEase(Ease.OutQuad);
+ }
}
if (confirmTimelineRoutine != null)
{
@@ -1156,7 +1299,6 @@ namespace AibisDream.MiniGame.Language
{
if (!particle.isRed)
{
- particle.isCalmed = true;
particle.hasEffect = false;
particle.isOrange = false;
particle.changeSpeedMultiplier = 1f;
@@ -1167,6 +1309,95 @@ namespace AibisDream.MiniGame.Language
{
particle.isCalmed = true;
}
+
+ // 释放感:冲击波 + 笑话粒子被吹散 + glitch 短脉冲
+ PlayCompletionBurst();
+ }
+
+ ///
+ /// 完成瞬间的释放表现:从目标簇质心放出冲击波环,非目标粒子被径向吹散并渐隐,
+ /// 延迟后进入平静态;同时打一次 glitch 短脉冲。
+ ///
+ private void PlayCompletionBurst()
+ {
+ Vector3 center = ComputeCentroid(targetParticles);
+
+ // 冲击波环(Shapes Ring 扩散淡出)
+ GameObject waveObj = new GameObject("CompletionShockwave");
+ waveObj.layer = renderLayer;
+ waveObj.transform.SetParent(transform, false);
+ float z = worldCanvas != null ? worldCanvas.transform.position.z : center.z;
+ waveObj.transform.position = new Vector3(center.x, center.y, z);
+
+ var wave = waveObj.AddComponent();
+ wave.Type = DiscType.Ring;
+ wave.Radius = 0.2f;
+ wave.Thickness = 0.12f;
+ Color waveColor = targetParticleColor;
+ waveColor.a = 0.9f;
+ wave.Color = waveColor;
+
+ float maxRadius = layoutShape == LayoutShape.Circle
+ ? boundsRadius
+ : Mathf.Max(movementBounds.extents.x, movementBounds.extents.y);
+ maxRadius *= 1.1f;
+ Color waveEnd = waveColor;
+ waveEnd.a = 0f;
+
+ Sequence waveSeq = DOTween.Sequence();
+ waveSeq.Append(DOTween.To(() => wave.Radius, r => wave.Radius = r, maxRadius, completionShockwaveDuration).SetEase(Ease.OutCubic));
+ waveSeq.Join(DOTween.To(() => wave.Thickness, t => wave.Thickness = t, 0.02f, completionShockwaveDuration).SetEase(Ease.OutQuad));
+ waveSeq.Join(DOTween.To(() => wave.Color, c => wave.Color = c, waveEnd, completionShockwaveDuration).SetEase(Ease.OutQuad));
+ waveSeq.OnComplete(() =>
+ {
+ if (waveObj != null)
+ Destroy(waveObj);
+ });
+ waveSeq.SetLink(waveObj);
+
+ // 非目标粒子径向吹散 + 渐隐
+ foreach (var particle in candidateParticles)
+ {
+ if (particle == null || particle.isRed)
+ continue;
+
+ Vector2 dir = (Vector2)(particle.transform.position - center);
+ dir = dir.sqrMagnitude < 0.0001f ? Random.insideUnitCircle.normalized : dir.normalized;
+ particle.isCalmed = false;
+ particle.ApplyForce(dir * completionBlastForce);
+
+ DOTween.To(() => particle.alpha, v => particle.alpha = v,
+ particle.baseAlpha * completionBlastAlphaRatio, completionCalmDelay)
+ .SetEase(Ease.OutQuad)
+ .SetTarget(particle);
+ }
+
+ if (completionBurstRoutine != null)
+ {
+ StopCoroutine(completionBurstRoutine);
+ }
+ completionBurstRoutine = StartCoroutine(CalmNonTargetsAfterBlast());
+
+ // glitch 短脉冲
+ if (completionGlitchPulsePeak > 0f && ExpressionManagerRef != null)
+ {
+ ExpressionManagerRef.PlayGlitchPulse(completionGlitchPulsePeak, 0.5f);
+ }
+ }
+
+ private IEnumerator CalmNonTargetsAfterBlast()
+ {
+ yield return new WaitForSeconds(completionCalmDelay);
+
+ foreach (var particle in candidateParticles)
+ {
+ if (particle != null && !particle.isRed)
+ {
+ particle.isCalmed = true;
+ particle.velocity = Vector2.zero;
+ }
+ }
+ completionBurstRoutine = null;
}
private List GetOrderedRedParticles()
@@ -1238,14 +1469,15 @@ namespace AibisDream.MiniGame.Language
interferenceLogicSuspended = true;
SetStatusUIVisibility(false, statusFadeOutDuration);
- // 两个 panel、confirm button、火山释放 log 界面在 focus 触发时淡出
- FadeOutUIElement(languageDeepPanel1, focusFadeDuration);
- FadeOutUIElement(languageDeepPanel2, focusFadeDuration);
+ // 两个 panel、confirm button、火山释放 log 界面在 focus 触发时淡出(与粒子聚焦移动同节奏)
+ float uiFadeDuration = FocusUiFadeDuration;
+ FadeOutUIElement(languageDeepPanel1, uiFadeDuration);
+ FadeOutUIElement(languageDeepPanel2, uiFadeDuration);
if (focusSequenceButton != null)
{
- FadeOutUIElement(focusSequenceButton.gameObject, focusFadeDuration);
+ FadeOutUIElement(focusSequenceButton.gameObject, uiFadeDuration);
}
- FadeOutSpriteRenderer(releaseLogSpriteRenderer, focusFadeDuration);
+ FadeOutSpriteRenderer(releaseLogSpriteRenderer, uiFadeDuration);
if (connectionRenderer != null)
{
@@ -1257,7 +1489,7 @@ namespace AibisDream.MiniGame.Language
{
particle.isCalmed = true;
particle.velocity = Vector2.zero;
- FadeOutTextParticle(particle, focusFadeDuration);
+ FadeOutTextParticle(particle, uiFadeDuration);
}
foreach (var particle in candidateParticles)
@@ -1266,7 +1498,7 @@ namespace AibisDream.MiniGame.Language
{
particle.isCalmed = true;
particle.velocity = Vector2.zero;
- FadeOutTextParticle(particle, focusFadeDuration);
+ FadeOutTextParticle(particle, uiFadeDuration);
}
else
{
@@ -1353,6 +1585,8 @@ namespace AibisDream.MiniGame.Language
Color ringColor = focusRingColor;
ringColor.a = focusRingAlpha;
disc.Color = ringColor;
+ // 不稳定期为旋转虚线环,锁定时收敛为实线(见 PlayFocusLockEffect)
+ disc.Dashed = true;
visual.ringDisc = disc;
GameObject labelObj = new GameObject("ProbabilityLabel");
@@ -1362,6 +1596,7 @@ namespace AibisDream.MiniGame.Language
rectTransform.localPosition = new Vector3(0f, estimatedRadius + 0.55f, 0f);
rectTransform.localScale = Vector3.one;
rectTransform.sizeDelta = new Vector2(2f, 0.6f);
+ visual.labelBasePos = rectTransform.localPosition;
var label = labelObj.AddComponent();
if (chineseFontAsset != null)
@@ -1370,12 +1605,106 @@ namespace AibisDream.MiniGame.Language
}
label.fontSize = 2f;
label.alignment = TextAlignmentOptions.Center;
- label.text = $"{Mathf.RoundToInt(visual.probability)}%";
+ Color labelColor = targetParticleColor;
+ labelColor.a = 0.9f;
+ label.color = labelColor;
+ label.text = FormatProbabilityText(visual.probability);
visual.probabilityLabel = label;
return visual;
}
+ /// 概率标签统一终端风格式
+ private static string FormatProbabilityText(float probability)
+ {
+ return $"> {Mathf.RoundToInt(probability)}%_";
+ }
+
+ ///
+ /// 不稳定期聚焦环虚线旋转 + 概率标签轻微抖动(每帧调用)
+ ///
+ private void UpdateFocusVisualIdle(TargetFocusVisual visual)
+ {
+ if (visual == null || visual.stabilized)
+ return;
+
+ if (visual.ringDisc != null)
+ {
+ visual.ringDisc.DashOffset += Time.deltaTime * focusRingDashRotateSpeed;
+ }
+
+ if (visual.probabilityLabel != null)
+ {
+ float jitter = 0.015f;
+ Vector3 offset = new Vector3(
+ (Mathf.PerlinNoise(Time.time * 9f, visual.orderIndex * 3.7f) - 0.5f) * jitter * 2f,
+ (Mathf.PerlinNoise(visual.orderIndex * 5.1f, Time.time * 9f) - 0.5f) * jitter * 2f,
+ 0f);
+ visual.probabilityLabel.rectTransform.localPosition = visual.labelBasePos + offset;
+ }
+ }
+
+ ///
+ /// 单字锁定表现:环收敛为实线并闪白后淡出,标签定格 100% 后淡出,字符弹跳强调
+ ///
+ private void PlayFocusLockEffect(TargetFocusVisual visual)
+ {
+ if (visual == null)
+ return;
+
+ if (visual.particle != null)
+ {
+ visual.particle.transform.DOPunchScale(
+ Vector3.one * (focusZoomScale * 0.15f), 0.25f, 6, 0.6f);
+ }
+
+ if (visual.ringDisc != null)
+ {
+ var disc = visual.ringDisc;
+ var ringObj = visual.ringObject;
+ disc.Dashed = false;
+
+ Color flashColor = Color.white;
+ flashColor.a = 1f;
+ Color fadeColor = focusRingColor;
+ fadeColor.a = 0f;
+ float baseRadius = disc.Radius;
+
+ Sequence seq = DOTween.Sequence();
+ seq.Append(DOTween.To(() => disc.Color, c => disc.Color = c, flashColor, 0.08f));
+ seq.Join(DOTween.To(() => disc.Radius, r => disc.Radius = r, baseRadius * 0.82f, 0.16f).SetEase(Ease.OutQuad));
+ seq.Append(DOTween.To(() => disc.Color, c => disc.Color = c, fadeColor, 0.4f).SetEase(Ease.OutQuad));
+ seq.OnComplete(() =>
+ {
+ if (ringObj != null)
+ Destroy(ringObj);
+ });
+ if (ringObj != null)
+ seq.SetLink(ringObj);
+
+ visual.ringDisc = null;
+ visual.ringObject = null;
+ }
+
+ if (visual.probabilityLabel != null)
+ {
+ var label = visual.probabilityLabel;
+ label.rectTransform.localPosition = visual.labelBasePos;
+ label.text = FormatProbabilityText(100f);
+ var labelSeq = DOTween.Sequence();
+ labelSeq.AppendInterval(0.3f);
+ labelSeq.Append(label.DOFade(0f, 0.3f));
+ labelSeq.OnComplete(() =>
+ {
+ if (label != null)
+ Destroy(label.gameObject);
+ });
+ labelSeq.SetLink(label.gameObject);
+
+ visual.probabilityLabel = null;
+ }
+ }
+
private void EnterFocusHolding()
{
completionPhase = CompletionPhase.FocusHolding;
@@ -1398,6 +1727,7 @@ namespace AibisDream.MiniGame.Language
Vector3 shuffleOffset = CalculateShuffleOffset(visual, 0f, focusTimer);
visual.particle.transform.position = visual.focusPosition + shuffleOffset;
+ UpdateFocusVisualIdle(visual);
}
}
@@ -1422,8 +1752,9 @@ namespace AibisDream.MiniGame.Language
float probability = Mathf.Lerp(visual.probability, visual.targetProbability, probabilityProgress);
if (visual.probabilityLabel != null)
{
- visual.probabilityLabel.text = $"{Mathf.RoundToInt(probability)}%";
+ visual.probabilityLabel.text = FormatProbabilityText(probability);
}
+ UpdateFocusVisualIdle(visual);
float interval = Mathf.Lerp(0.05f, 0.8f, probabilityProgress);
visual.particle.SetChangeInterval(interval);
@@ -1442,10 +1773,7 @@ namespace AibisDream.MiniGame.Language
visual.particle.ResetChangeTimer(visual.particle.changeSpeedMultiplier);
visual.particle.isStatic = true;
visual.stabilized = true;
- if (visual.probabilityLabel != null)
- {
- visual.probabilityLabel.text = "100%";
- }
+ PlayFocusLockEffect(visual);
}
}
@@ -1550,6 +1878,7 @@ namespace AibisDream.MiniGame.Language
DestroyFocusVisualDecorationsAndClear();
foreach (var p in particlesToScale)
{
+ p.transform.DOKill();
p.transform.DOScale(Vector3.one * focusZoomScale, 0.3f).SetEase(Ease.OutQuad);
p.isStatic = true;
p.changeSpeedMultiplier = 1f;
@@ -1754,7 +2083,11 @@ namespace AibisDream.MiniGame.Language
private Vector2 GetMouseWorldPosition()
{
Vector3 mousePos = Input.mousePosition;
- Camera cam = Camera.main;
+ if (cachedMainCamera == null)
+ {
+ cachedMainCamera = Camera.main;
+ }
+ Camera cam = cachedMainCamera;
if (cam != null)
{
// 对于正交相机,需要使用正确的Z距离
@@ -1777,12 +2110,19 @@ namespace AibisDream.MiniGame.Language
DestroyFocusVisualDecorationsAndClear();
finalTargetCharacters.Clear();
finalTargetPositions.Clear();
- previousConnections.Clear();
initializationComplete = false;
initializationFrames = 0;
focusTweensCompleted = false;
interferenceLogicSuspended = false;
ClearCompletionClampSmoothVelocity();
+ displayedIntegrationPercent = 0f;
+ displayedInterferenceCount = 0f;
+ lastInterferenceCount = -1;
+ if (interferenceCountText != null && interferenceTextBaseColorCaptured)
+ {
+ DOTween.Kill(interferenceCountText);
+ interferenceCountText.color = interferenceTextBaseColor;
+ }
SetStatusUIVisibility(true, 0f);
if (entranceRoutine != null)
{
@@ -1802,6 +2142,12 @@ namespace AibisDream.MiniGame.Language
expressionPanelRoutine = null;
}
+ if (completionBurstRoutine != null)
+ {
+ StopCoroutine(completionBurstRoutine);
+ completionBurstRoutine = null;
+ }
+
if (focusSequenceButton != null)
{
focusSequenceButton.onClick.RemoveListener(HandleFocusButtonClicked);
@@ -1840,7 +2186,9 @@ namespace AibisDream.MiniGame.Language
p.alpha = p.baseAlpha;
p.transform.localScale = Vector3.one;
p.transform.DOKill();
- p.SetInterferenceParams(nonTargetShakeIntensity, nonTargetShakeSpeed, nonTargetChangeInterval, nonTargetFloatAmplitude);
+ DOTween.Kill(p);
+ EnsureActiveInterferenceParams();
+ p.SetInterferenceParams(activeShakeIntensity, activeShakeSpeed, activeChangeInterval, activeFloatAmplitude);
if (p.glowSprite != null)
{
@@ -1964,6 +2312,7 @@ namespace AibisDream.MiniGame.Language
if (!gameObject.activeInHierarchy || candidateParticles.Count == 0)
{
Debug.LogWarning($"[LanguageParticleManager] PlayEntranceSequence 跳过: activeInHierarchy={gameObject.activeInHierarchy}, candidateParticles.Count={candidateParticles.Count} (不会播放火山表达panel)");
+ entranceVisualsFinished = true;
allowInput = true; // 否则 allowInput 会一直为 false
return;
}
@@ -1979,12 +2328,14 @@ namespace AibisDream.MiniGame.Language
private IEnumerator EntranceSequenceRoutine()
{
allowInput = false;
+ entranceVisualsFinished = false;
entranceTargets.Clear();
Vector3 burstOrigin = worldCanvas != null ? worldCanvas.transform.position : transform.position;
List particles = candidateParticles.Where(p => p != null).ToList();
if (particles.Count == 0)
{
+ entranceVisualsFinished = true;
allowInput = true;
entranceRoutine = null;
yield break;
@@ -2072,7 +2423,15 @@ namespace AibisDream.MiniGame.Language
float waitTime = Mathf.Max(maxCoreTime, cascadeDuration);
if (waitTime > 0f)
{
- yield return new WaitForSeconds(waitTime + 0.2f);
+ // cascade 进行到指定比例时并行启动火山表达panel,消除"粒子播完才出 UI"的空档
+ float panelDelay = waitTime * Mathf.Clamp01(panelStartAtEntranceProgress);
+ yield return new WaitForSeconds(panelDelay);
+ StartExpressionPanelRoutine();
+ yield return new WaitForSeconds(waitTime - panelDelay + 0.2f);
+ }
+ else
+ {
+ StartExpressionPanelRoutine();
}
foreach (var candidate in particles)
@@ -2083,9 +2442,12 @@ namespace AibisDream.MiniGame.Language
}
entranceTargets.Clear();
+ entranceVisualsFinished = true;
entranceRoutine = null;
+ }
- // 开场表现完成,启动独立的火山表达panel协程(避免与 Entrance 耦合)
+ private void StartExpressionPanelRoutine()
+ {
if (expressionPanelRoutine != null)
{
StopCoroutine(expressionPanelRoutine);
@@ -2126,6 +2488,12 @@ namespace AibisDream.MiniGame.Language
Debug.LogWarning("[LanguageParticleManager] TimelineCenter.Instance 为空,无法播放火山表达panel");
}
+ // panel 与入场表现并行,两者都完成才解锁输入
+ while (!entranceVisualsFinished)
+ {
+ yield return null;
+ }
+
allowInput = true;
expressionPanelRoutine = null;
}
@@ -2169,6 +2537,63 @@ namespace AibisDream.MiniGame.Language
}
}
+ ///
+ /// 非目标粒子向最近目标粒子的轻微漂移偏置(仅观感"笑话往真话凑",远小于玩家施力)
+ ///
+ private void ApplyInterferenceDrift()
+ {
+ if (activeDriftBias <= 0f || isCompleted || targetParticles.Count == 0)
+ return;
+
+ foreach (var particle in candidateParticles)
+ {
+ if (particle == null || particle.isRed || particle.isStatic || particle.isCalmed)
+ continue;
+
+ Vector2 pos = particle.transform.position;
+ CandidateParticle nearest = null;
+ float bestSqr = float.MaxValue;
+ foreach (var t in targetParticles)
+ {
+ if (t == null) continue;
+ float sqr = ((Vector2)t.transform.position - pos).sqrMagnitude;
+ if (sqr < bestSqr)
+ {
+ bestSqr = sqr;
+ nearest = t;
+ }
+ }
+
+ if (nearest == null || bestSqr < 0.01f)
+ continue;
+
+ Vector2 dir = ((Vector2)nearest.transform.position - pos).normalized;
+ particle.ApplyForce(dir * (activeDriftBias * Time.deltaTime));
+ }
+ }
+
+ ///
+ /// 游玩期 glitch 基线随干扰计数轻微浮动(每 0.5s 更新一次;yarn 手动驱动后自动失效)
+ ///
+ private void UpdateInterferenceGlitchLink()
+ {
+ if (activeGlitchBaselineMax <= 0f || isCompleted || !allowInput)
+ return;
+
+ glitchLinkTimer += Time.deltaTime;
+ if (glitchLinkTimer < 0.5f)
+ return;
+ glitchLinkTimer = 0f;
+
+ var em = ExpressionManagerRef;
+ if (em == null)
+ return;
+
+ // 以 6 处干扰为满强度参照,映射到 0~baselineMax
+ float normalized = Mathf.Clamp01(CountInterferenceNodes() / 6f);
+ em.SetGameplayGlitchBaseline(normalized * activeGlitchBaselineMax, 0.6f);
+ }
+
private void UpdateStatusUI()
{
if (statusUIHidden)
@@ -2176,18 +2601,48 @@ namespace AibisDream.MiniGame.Language
if (integrationProgressText != null)
{
- float ratio = CalculateIntegrationRatio();
- int percent = Mathf.RoundToInt(ratio * 100f);
- integrationProgressText.text = $"{percent}% 语言整合完成度";
+ float targetPercent = CalculateIntegrationRatio() * 100f;
+ displayedIntegrationPercent = Mathf.MoveTowards(
+ displayedIntegrationPercent, targetPercent, statusCountUpSpeed * Time.deltaTime);
+ integrationProgressText.text = $"{Mathf.RoundToInt(displayedIntegrationPercent)}% 语言整合完成度";
}
if (interferenceCountText != null)
{
int interferenceCount = CountInterferenceNodes();
- interferenceCountText.text = $"{interferenceCount} 处异常思绪仍在干扰表达";
+ if (lastInterferenceCount >= 0 && interferenceCount < lastInterferenceCount)
+ {
+ PulseInterferenceText();
+ }
+ lastInterferenceCount = interferenceCount;
+
+ displayedInterferenceCount = Mathf.MoveTowards(
+ displayedInterferenceCount, interferenceCount, statusCountUpSpeed * 0.3f * Time.deltaTime);
+ interferenceCountText.text = $"{Mathf.RoundToInt(displayedInterferenceCount)} 处异常思绪仍在干扰表达";
}
}
+ ///
+ /// 干扰计数下降时闪一下目标色,给玩家正反馈
+ ///
+ private void PulseInterferenceText()
+ {
+ if (interferenceCountText == null || statusUIHidden || !interferenceTextBaseColorCaptured)
+ return;
+
+ DOTween.Kill(interferenceCountText);
+ Color pulse = targetParticleColor;
+ pulse.a = interferenceTextBaseAlpha;
+ Color baseColor = interferenceTextBaseColor;
+ baseColor.a = interferenceTextBaseAlpha;
+
+ Sequence seq = DOTween.Sequence().SetTarget(interferenceCountText);
+ seq.Append(DOTween.To(
+ () => interferenceCountText.color, c => interferenceCountText.color = c, pulse, 0.08f));
+ seq.Append(DOTween.To(
+ () => interferenceCountText.color, c => interferenceCountText.color = c, baseColor, 0.4f));
+ }
+
private float CalculateIntegrationRatio()
{
if (targetParticles.Count == 0)
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
index 0b5ed66a9..26da4f431 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageYarnCommand.cs
@@ -19,27 +19,32 @@ namespace AibisDream.MiniGame.Language
/// 注意:Yarn Spinner 2.x 对同名 重载支持不可靠,只保留一个注册入口,用可选参数区分 3/4 参调用。
/// <>
/// <>
+ /// <>
/// (第四项为「非目标」候选粒子总数,候选池 = 目标句字数 + 该值;不写第四项则用 Inspector 的 Candidate Count)
+ /// (第五项为干扰表现档位索引:变字节奏/抖动/glitch联动按档递进,不写则用 Inspector 全局干扰参数)
///
/// 干扰短句(笑话等),用 | 分隔,用于非目标粒子字符池,如:"哈哈|嘿嘿|呵呵"
/// 目标句子
/// 完成时触发的对话节点(可空字符串)
/// 非目标候选粒子总数;<0 表示使用 Inspector 默认
+ /// 干扰表现档位索引;<0 表示使用 Inspector 全局干扰参数
[YarnCommand("start_expression")]
public static IEnumerator StartExpression(
string anxietyPhrasesStr,
string targetSentence,
string completionNode = "",
- float nonTargetParticleTotal = -1f)
+ float nonTargetParticleTotal = -1f,
+ float interferencePresetIndex = -1f)
{
return RunStartExpression(
anxietyPhrasesStr,
targetSentence,
completionNode ?? "",
- UnityEngine.Mathf.RoundToInt(nonTargetParticleTotal));
+ UnityEngine.Mathf.RoundToInt(nonTargetParticleTotal),
+ UnityEngine.Mathf.RoundToInt(interferencePresetIndex));
}
- private static IEnumerator RunStartExpression(string anxietyPhrasesStr, string targetSentence, string completionNode, int nonTargetParticleTotal)
+ private static IEnumerator RunStartExpression(string anxietyPhrasesStr, string targetSentence, string completionNode, int nonTargetParticleTotal, int interferencePresetIndex)
{
if (ExpressionManager == null)
{
@@ -60,7 +65,7 @@ namespace AibisDream.MiniGame.Language
// 先淡出上一轮文字/连线等,再淡入火山释放 log,最后开新一轮粒子(避免 log 与旧字叠在一起)
yield return ExpressionManager.FadeOutParticlesBeforeNewRound(0f);
yield return ExpressionManager.EnsureReleaseLogFadedIn(1f);
- ExpressionManager.StartSystem(phrases, targetSentence, completionNode, nonTargetParticleTotal);
+ ExpressionManager.StartSystem(phrases, targetSentence, completionNode, nonTargetParticleTotal, interferencePresetIndex);
yield return ExpressionManager.WaitUntilExpressionFlowReady();
}
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/README_语言系统使用说明.md b/Assets/Scripts/MiniGame/HuoShan/Language/README_语言系统使用说明.md
index 947b6cc97..fe36dbd3e 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/README_语言系统使用说明.md
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/README_语言系统使用说明.md
@@ -34,12 +34,15 @@
**语法:**
```yarn
<>
+<>
```
**参数说明:**
- **焦虑短句**(必填):多个短句用 `|` 分隔,这些短句会显示在非目标字符上,表示焦虑情绪
- **目标句子**(必填):玩家需要完成的目标句子,系统会根据字数设置目标字符数量
- **完成对话节点**(可选):游戏完成时触发的对话节点名称,不填则不触发
+- **非目标粒子总数**(可选,第4参):候选池 = 目标句字数 + 该值;不填用 Inspector 的 Candidate Count
+- **干扰表现档位**(可选,第5参):`LanguageParticleManager` Inspector 上 `Interference Presets` 的索引(0/1/2 递进)。只影响非目标粒子的变字节奏、抖动、漂浮、向目标簇的轻微漂移观感和 glitch 联动强度,**不实质增加操作难度**;不填用 Inspector 全局干扰参数
**示例:**
```yarn
diff --git a/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn b/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn
index 9b4977c2f..af6bd41cd 100644
--- a/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn
+++ b/Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn
@@ -55,11 +55,11 @@ hs: 好…吧…… #line:02115ea
// LOG1: 简单 - 4个目标粒子
// 主题:火山的恐惧
// 干扰词:笑话碎片(短小,2字)
-// 难度:简单 - 干扰少,粒子少(非目标 16)
+// 难度:简单 - 干扰少,粒子少(非目标 10)
// ═══════════════════════════════════════════════════════════════
me: 这一条看起来……是一个关于焦虑的记录。 #line:046ab93d
//me: NOTE:或许能找合适的方式让这句话的上下文融进来 #line:05217f0
-<>
+<>
===
@@ -103,11 +103,11 @@ hs: 它们都说我是…… #line:0a8663b
// LOG2: 中等 - 6个目标粒子
// 主题:关于英里的事
// 干扰词:更多笑话碎片(3字)
-// 难度:中等 - 干扰增加,粒子增加(非目标 28)
+// 难度:中等 - 干扰略增,表现更躁(非目标 16,表现档1)
// ═══════════════════════════════════════════════════════════════
<>
me: 这里又有一条阻塞的log,是关于恐惧的。 #line:03bf85f
-<>
+<>
===
@@ -146,11 +146,11 @@ me: 对不起…火山。 #line:03686b2a
// LOG3: 困难 - 8个目标粒子
// 主题:自责与释放
// 干扰词:更多更长的笑话碎片(4-5字),干扰更强
-// 难度:困难 - 最多干扰,最多粒子(非目标 40)
+// 难度:困难 - 干扰最多靠表现传达(非目标 22,表现档2)
// ═══════════════════════════════════════════════════════════════
<>
me: 最后一个log……这是…关于愤怒的。 #line:052abcd
-<>
+<>
===