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 -<> +<> ===