diff --git a/Assets/Config.meta b/Assets/Config.meta
new file mode 100644
index 000000000..fe70da050
--- /dev/null
+++ b/Assets/Config.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d88539d912cafbe489fa8efdd1fdf0b9
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Config/EmotionWave.meta b/Assets/Config/EmotionWave.meta
new file mode 100644
index 000000000..85a22c7c8
--- /dev/null
+++ b/Assets/Config/EmotionWave.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d558d580689c5ba4c9981eee8e436b82
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Config/EmotionWave/DefaultEmotionWaveConfig.asset b/Assets/Config/EmotionWave/DefaultEmotionWaveConfig.asset
new file mode 100644
index 000000000..6ef1e012f
--- /dev/null
+++ b/Assets/Config/EmotionWave/DefaultEmotionWaveConfig.asset
@@ -0,0 +1,375 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e4cbad02f49607e4c873f5e76ed20513, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ anchors:
+ - position: 0
+ name: Raw Signal
+ stage: BUILD
+ waveParams:
+ waveMode: 0
+ timeSpread: 0.94
+ omegaX: 1.2
+ omegaY: 1
+ phaseSpeed: 0.07
+ numCycles: 2
+ amplitude: 0.36
+ amplitudeY: 0.2
+ waveformType: 0
+ spikiness: 0
+ waveshapeGain: 1
+ harmonicStrength: 0
+ harmonicFreq: 2
+ noiseAmount: 0
+ noiseScale: 0.5
+ noiseSpeed: 0.12
+ jitter: 0
+ weierstrassAmount: 0
+ lineWidth: 0.88
+ persistence: 0.86
+ fragmentation: 0
+ hue: 128
+ saturation: 52
+ lightness: 32
+ glowIntensity: 0.45
+ breathRate: 0.12
+ breathDepth: 0.008
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 0.15
+ name: Harmonic Stack
+ stage: BUILD
+ waveParams:
+ waveMode: 0.32
+ timeSpread: 0.76
+ omegaX: 2
+ omegaY: 1.5
+ phaseSpeed: 0.24
+ numCycles: 2
+ amplitude: 0.4
+ amplitudeY: 0.38
+ waveformType: 0
+ spikiness: 0
+ waveshapeGain: 1
+ harmonicStrength: 0.04
+ harmonicFreq: 2.6
+ noiseAmount: 0.008
+ noiseScale: 0.7
+ noiseSpeed: 0.28
+ jitter: 0.003
+ weierstrassAmount: 0
+ lineWidth: 1.15
+ persistence: 0.88
+ fragmentation: 0
+ hue: 128
+ saturation: 55
+ lightness: 36
+ glowIntensity: 0.58
+ breathRate: 0.28
+ breathDepth: 0.022
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 0.25
+ name: Transition
+ stage: BUILD
+ waveParams:
+ waveMode: 0.55
+ timeSpread: 0.62
+ omegaX: 1.85
+ omegaY: 1.15
+ phaseSpeed: 0.26
+ numCycles: 2
+ amplitude: 0.43
+ amplitudeY: 0.42
+ waveformType: 0
+ spikiness: 0
+ waveshapeGain: 1
+ harmonicStrength: 0.022
+ harmonicFreq: 2.2
+ noiseAmount: 0.004
+ noiseScale: 0.65
+ noiseSpeed: 0.24
+ jitter: 0.001
+ weierstrassAmount: 0
+ lineWidth: 1.22
+ persistence: 0.875
+ fragmentation: 0
+ hue: 128
+ saturation: 55
+ lightness: 37
+ glowIntensity: 0.6
+ breathRate: 0.3
+ breathDepth: 0.024
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 0.35
+ name: Language Formed
+ stage: BUILD
+ waveParams:
+ waveMode: 1
+ timeSpread: 0.5
+ omegaX: 2
+ omegaY: 1
+ phaseSpeed: 0.3
+ numCycles: 2
+ amplitude: 0.45
+ amplitudeY: 0.45
+ waveformType: 0
+ spikiness: 0
+ waveshapeGain: 1
+ harmonicStrength: 0
+ harmonicFreq: 2
+ noiseAmount: 0
+ noiseScale: 0.8
+ noiseSpeed: 0.3
+ jitter: 0
+ weierstrassAmount: 0
+ lineWidth: 1.3
+ persistence: 0.87
+ fragmentation: 0
+ hue: 128
+ saturation: 55
+ lightness: 38
+ glowIntensity: 0.6
+ breathRate: 0.32
+ breathDepth: 0.025
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 0.5
+ name: Deep Review
+ stage: REVIEW
+ waveParams:
+ waveMode: 1
+ timeSpread: 0.5
+ omegaX: 2
+ omegaY: 1
+ phaseSpeed: 0.5
+ numCycles: 2
+ amplitude: 0.48
+ amplitudeY: 0.48
+ waveformType: 0
+ spikiness: 0
+ waveshapeGain: 1
+ harmonicStrength: 0
+ harmonicFreq: 2
+ noiseAmount: 0
+ noiseScale: 1
+ noiseSpeed: 0.3
+ jitter: 0
+ weierstrassAmount: 0
+ lineWidth: 1.3
+ persistence: 0.87
+ fragmentation: 0
+ hue: 128
+ saturation: 55
+ lightness: 40
+ glowIntensity: 0.65
+ breathRate: 0.38
+ breathDepth: 0.022
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 0.65
+ name: Review Pass
+ stage: REVIEW
+ waveParams:
+ waveMode: 1
+ timeSpread: 0.5
+ omegaX: 2
+ omegaY: 1
+ phaseSpeed: 0.35
+ numCycles: 2
+ amplitude: 0.48
+ amplitudeY: 0.48
+ waveformType: 0
+ spikiness: 0
+ waveshapeGain: 1
+ harmonicStrength: 0
+ harmonicFreq: 2
+ noiseAmount: 0
+ noiseScale: 0.9
+ noiseSpeed: 0.25
+ jitter: 0
+ weierstrassAmount: 0
+ lineWidth: 1.3
+ persistence: 0.87
+ fragmentation: 0
+ hue: 128
+ saturation: 55
+ lightness: 40
+ glowIntensity: 0.6
+ breathRate: 0.32
+ breathDepth: 0.02
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 0.75
+ name: Tone Tweak
+ stage: ADJUST
+ waveParams:
+ waveMode: 1
+ timeSpread: 0.5
+ omegaX: 2
+ omegaY: 1
+ phaseSpeed: 0.35
+ numCycles: 2
+ amplitude: 0.48
+ amplitudeY: 0.48
+ waveformType: 0
+ spikiness: 0
+ waveshapeGain: 1
+ harmonicStrength: 0
+ harmonicFreq: 2
+ noiseAmount: 0
+ noiseScale: 0.9
+ noiseSpeed: 0.25
+ jitter: 0
+ weierstrassAmount: 0
+ lineWidth: 1.3
+ persistence: 0.87
+ fragmentation: 0
+ hue: 128
+ saturation: 55
+ lightness: 40
+ glowIntensity: 0.6
+ breathRate: 0.3
+ breathDepth: 0.02
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 0.85
+ name: Error Compress
+ stage: ADJUST
+ waveParams:
+ waveMode: 1
+ timeSpread: 0.5
+ omegaX: 2.5
+ omegaY: 1.5
+ phaseSpeed: 0.45
+ numCycles: 2
+ amplitude: 0.48
+ amplitudeY: 0.48
+ waveformType: 1
+ spikiness: 0
+ waveshapeGain: 3
+ harmonicStrength: 0.04
+ harmonicFreq: 2
+ noiseAmount: 0.012
+ noiseScale: 1
+ noiseSpeed: 0.4
+ jitter: 0.006
+ weierstrassAmount: 0.22
+ lineWidth: 1.35
+ persistence: 0.84
+ fragmentation: 0
+ hue: 50
+ saturation: 56
+ lightness: 46
+ glowIntensity: 0.7
+ breathRate: 0.42
+ breathDepth: 0.025
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
+ - position: 1
+ name: Joke Spike
+ stage: ADJUST
+ waveParams:
+ waveMode: 1
+ timeSpread: 0.5
+ omegaX: 3
+ omegaY: 2
+ phaseSpeed: 0.5
+ numCycles: 2
+ amplitude: 0.48
+ amplitudeY: 0.48
+ waveformType: 2
+ spikiness: 0
+ waveshapeGain: 5
+ harmonicStrength: 0.08
+ harmonicFreq: 2
+ noiseAmount: 0.02
+ noiseScale: 1.2
+ noiseSpeed: 0.5
+ jitter: 0.01
+ weierstrassAmount: 0.42
+ lineWidth: 1.4
+ persistence: 0.82
+ fragmentation: 0
+ hue: 0
+ saturation: 58
+ lightness: 50
+ glowIntensity: 0.78
+ breathRate: 0.52
+ breathDepth: 0.03
+ reviewIntensity: 0
+ featureBoxRate: 0
+ thresholdLevel: 0
+ cursorSpeed: 0
+ spikeIntensity: 0
+ spikeFreq: 0
+ spikeSharpness: 0
+ envelopeDecay: 0
+ tipLayer: 0
diff --git a/Assets/Config/EmotionWave/DefaultEmotionWaveConfig.asset.meta b/Assets/Config/EmotionWave/DefaultEmotionWaveConfig.asset.meta
new file mode 100644
index 000000000..4ca872527
--- /dev/null
+++ b/Assets/Config/EmotionWave/DefaultEmotionWaveConfig.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9c046a1bc8b00d149b08de4b96288243
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/ColorUtil.cs b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/ColorUtil.cs
new file mode 100644
index 000000000..f236d4b69
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/ColorUtil.cs
@@ -0,0 +1,37 @@
+using UnityEngine;
+
+namespace AibisDream.MiniGame.HuoShan.EmotionWave
+{
+ public static class ColorUtil
+ {
+ ///
+ /// HSL to RGB conversion matching CSS hsla(h, s%, l%, a).
+ /// h: 0~360, s: 0~100, l: 0~100, a: 0~1
+ ///
+ public static Color HslToRgb(float h, float s, float l, float a = 1f)
+ {
+ h = ((h % 360f) + 360f) % 360f;
+ float sNorm = Mathf.Clamp01(s / 100f);
+ float lNorm = Mathf.Clamp01(l / 100f);
+
+ float c = (1f - Mathf.Abs(2f * lNorm - 1f)) * sNorm;
+ float x = c * (1f - Mathf.Abs((h / 60f) % 2f - 1f));
+ float m = lNorm - c * 0.5f;
+
+ float r, g, b;
+ if (h < 60f) { r = c; g = x; b = 0f; }
+ else if (h < 120f) { r = x; g = c; b = 0f; }
+ else if (h < 180f) { r = 0f; g = c; b = x; }
+ else if (h < 240f) { r = 0f; g = x; b = c; }
+ else if (h < 300f) { r = x; g = 0f; b = c; }
+ else { r = c; g = 0f; b = x; }
+
+ return new Color(
+ Mathf.Clamp01(r + m),
+ Mathf.Clamp01(g + m),
+ Mathf.Clamp01(b + m),
+ Mathf.Clamp01(a)
+ );
+ }
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/ColorUtil.cs.meta b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/ColorUtil.cs.meta
new file mode 100644
index 000000000..50a7b418c
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/ColorUtil.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: da528dd9fdd5afb4093ea165b5ec4a55
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveConfig.cs b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveConfig.cs
new file mode 100644
index 000000000..a3b60c53e
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveConfig.cs
@@ -0,0 +1,209 @@
+using UnityEngine;
+
+namespace AibisDream.MiniGame.HuoShan.EmotionWave
+{
+ [CreateAssetMenu(fileName = "EmotionWaveConfig", menuName = "AibisDream/EmotionWave Config")]
+ public class EmotionWaveConfig : ScriptableObject
+ {
+ [System.Serializable]
+ public struct WaveParams
+ {
+ [Header("Trajectory Shape")]
+ [Range(0f, 1f)] public float waveMode;
+ [Range(0.1f, 1f)] public float timeSpread;
+ public float omegaX;
+ public float omegaY;
+ public float phaseSpeed;
+ [Min(1)] public int numCycles;
+
+ [Header("Amplitude & Waveform")]
+ public float amplitude;
+ public float amplitudeY;
+ [Range(0f, 2f)] public float waveformType;
+ [Range(0f, 1f)] public float spikiness;
+ [Range(1f, 12f)] public float waveshapeGain;
+ public float harmonicStrength;
+ public float harmonicFreq;
+
+ [Header("Noise & Glitch")]
+ public float noiseAmount;
+ public float noiseScale;
+ public float noiseSpeed;
+ public float jitter;
+ [Range(0f, 1f)] public float weierstrassAmount;
+
+ [Header("Line")]
+ public float lineWidth;
+ [Range(0.5f, 0.98f)] public float persistence;
+ [Range(0f, 1f)] public float fragmentation;
+
+ [Header("Color (HSL)")]
+ public float hue;
+ [Range(0f, 100f)] public float saturation;
+ [Range(5f, 80f)] public float lightness;
+ [Range(0f, 1.2f)] public float glowIntensity;
+
+ [Header("Dynamics")]
+ public float breathRate;
+ public float breathDepth;
+
+ [Header("Review (injected at runtime)")]
+ [HideInInspector] public float reviewIntensity;
+ [HideInInspector] public float featureBoxRate;
+ [HideInInspector] public float thresholdLevel;
+ [HideInInspector] public float cursorSpeed;
+
+ [Header("Spike")]
+ [HideInInspector] public float spikeIntensity;
+ [HideInInspector] public float spikeFreq;
+ [HideInInspector] public float spikeSharpness;
+ [HideInInspector] public float envelopeDecay;
+ [HideInInspector] public float tipLayer;
+
+ public static WaveParams Lerp(WaveParams a, WaveParams b, float t)
+ {
+ return new WaveParams
+ {
+ waveMode = Mathf.Lerp(a.waveMode, b.waveMode, t),
+ timeSpread = Mathf.Lerp(a.timeSpread, b.timeSpread, t),
+ omegaX = Mathf.Lerp(a.omegaX, b.omegaX, t),
+ omegaY = Mathf.Lerp(a.omegaY, b.omegaY, t),
+ phaseSpeed = Mathf.Lerp(a.phaseSpeed, b.phaseSpeed, t),
+ numCycles = Mathf.RoundToInt(Mathf.Lerp(a.numCycles, b.numCycles, t)),
+ amplitude = Mathf.Lerp(a.amplitude, b.amplitude, t),
+ amplitudeY = Mathf.Lerp(a.amplitudeY, b.amplitudeY, t),
+ waveformType = Mathf.Lerp(a.waveformType, b.waveformType, t),
+ spikiness = Mathf.Lerp(a.spikiness, b.spikiness, t),
+ waveshapeGain = Mathf.Lerp(a.waveshapeGain, b.waveshapeGain, t),
+ harmonicStrength = Mathf.Lerp(a.harmonicStrength, b.harmonicStrength, t),
+ harmonicFreq = Mathf.Lerp(a.harmonicFreq, b.harmonicFreq, t),
+ noiseAmount = Mathf.Lerp(a.noiseAmount, b.noiseAmount, t),
+ noiseScale = Mathf.Lerp(a.noiseScale, b.noiseScale, t),
+ noiseSpeed = Mathf.Lerp(a.noiseSpeed, b.noiseSpeed, t),
+ jitter = Mathf.Lerp(a.jitter, b.jitter, t),
+ weierstrassAmount = Mathf.Lerp(a.weierstrassAmount, b.weierstrassAmount, t),
+ lineWidth = Mathf.Lerp(a.lineWidth, b.lineWidth, t),
+ persistence = Mathf.Lerp(a.persistence, b.persistence, t),
+ fragmentation = Mathf.Lerp(a.fragmentation, b.fragmentation, t),
+ hue = Mathf.Lerp(a.hue, b.hue, t),
+ saturation = Mathf.Lerp(a.saturation, b.saturation, t),
+ lightness = Mathf.Lerp(a.lightness, b.lightness, t),
+ glowIntensity = Mathf.Lerp(a.glowIntensity, b.glowIntensity, t),
+ breathRate = Mathf.Lerp(a.breathRate, b.breathRate, t),
+ breathDepth = Mathf.Lerp(a.breathDepth, b.breathDepth, t),
+ };
+ }
+
+ public void Clamp()
+ {
+ numCycles = Mathf.Max(2, numCycles);
+ amplitude = Mathf.Max(0.05f, amplitude);
+ amplitudeY = Mathf.Max(0.05f, amplitudeY);
+ lineWidth = Mathf.Max(0.3f, lineWidth);
+ persistence = Mathf.Clamp(persistence, 0.5f, 0.98f);
+ glowIntensity = Mathf.Clamp(glowIntensity, 0f, 1.2f);
+ noiseAmount = Mathf.Max(0f, noiseAmount);
+ fragmentation = Mathf.Clamp01(fragmentation);
+ hue = ((hue % 360f) + 360f) % 360f;
+ saturation = Mathf.Clamp(saturation, 0f, 100f);
+ lightness = Mathf.Clamp(lightness, 5f, 80f);
+ waveMode = Mathf.Clamp01(waveMode);
+ spikiness = Mathf.Clamp01(spikiness);
+ waveformType = Mathf.Clamp(waveformType, 0f, 2f);
+ timeSpread = Mathf.Clamp(timeSpread, 0.1f, 1f);
+ waveshapeGain = Mathf.Clamp(waveshapeGain, 1f, 12f);
+ weierstrassAmount = Mathf.Clamp01(weierstrassAmount);
+ }
+ }
+
+ [System.Serializable]
+ public struct StageAnchor
+ {
+ public float position;
+ public string name;
+ public string stage;
+ public WaveParams waveParams;
+ }
+
+ public StageAnchor[] anchors = new StageAnchor[9];
+
+ static float Smoothstep(float t)
+ {
+ t = Mathf.Clamp01(t);
+ return t * t * (3f - 2f * t);
+ }
+
+ public WaveParams Interpolate(float value)
+ {
+ value = Mathf.Clamp01(value);
+ int n = anchors.Length;
+ if (n == 0) return default;
+ if (n == 1) return anchors[0].waveParams;
+
+ int idx = 0;
+ for (int i = 0; i < n - 1; i++)
+ {
+ if (value <= anchors[i + 1].position)
+ {
+ idx = i;
+ break;
+ }
+ if (i == n - 2) idx = n - 2;
+ }
+
+ int i1 = idx;
+ int i2 = Mathf.Min(n - 1, idx + 1);
+
+ float segStart = anchors[i1].position;
+ float segEnd = anchors[i2].position;
+ float segLen = segEnd - segStart;
+ float tLinear = segLen > 0f ? (value - segStart) / segLen : 0f;
+ float t = Smoothstep(tLinear);
+
+ var result = WaveParams.Lerp(anchors[i1].waveParams, anchors[i2].waveParams, t);
+ result.Clamp();
+ return result;
+ }
+
+ public string GetStageName(float value)
+ {
+ value = Mathf.Clamp01(value);
+ if (anchors == null || anchors.Length == 0) return "";
+
+ StageAnchor closest = anchors[0];
+ float minDist = float.MaxValue;
+ foreach (var a in anchors)
+ {
+ float d = Mathf.Abs(value - a.position);
+ if (d < minDist) { minDist = d; closest = a; }
+ }
+ if (minDist < 0.03f) return closest.name;
+
+ StageAnchor lower = anchors[0];
+ StageAnchor upper = anchors[anchors.Length - 1];
+ for (int i = 0; i < anchors.Length - 1; i++)
+ {
+ if (value >= anchors[i].position && value <= anchors[i + 1].position)
+ {
+ lower = anchors[i];
+ upper = anchors[i + 1];
+ break;
+ }
+ }
+
+ float blend = (upper.position - lower.position) > 0.001f
+ ? (value - lower.position) / (upper.position - lower.position)
+ : 0f;
+ if (blend < 0.35f) return lower.name;
+ if (blend > 0.65f) return upper.name;
+ return $"{lower.name} -> {upper.name}";
+ }
+
+ public string GetStagePhase(float value)
+ {
+ if (value < 0.35f) return "BUILD";
+ if (value < 0.65f) return "REVIEW";
+ return "ADJUST";
+ }
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveConfig.cs.meta b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveConfig.cs.meta
new file mode 100644
index 000000000..c98847d10
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e4cbad02f49607e4c873f5e76ed20513
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveController.cs b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveController.cs
new file mode 100644
index 000000000..4ec6748f7
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveController.cs
@@ -0,0 +1,134 @@
+using UnityEngine;
+
+namespace AibisDream.MiniGame.HuoShan.EmotionWave
+{
+ ///
+ /// Controls the EmotionWave visualization.
+ /// Drag the progress slider in the Inspector to drive the 3-act narrative.
+ /// Ported from Web_EmotionWave/js/app.js.
+ ///
+ [ExecuteAlways]
+ public class EmotionWaveController : MonoBehaviour
+ {
+ [Header("References")]
+ [SerializeField] private EmotionWaveConfig config;
+ [SerializeField] private EmotionWaveRenderer waveRenderer;
+
+ [Header("Progress Control")]
+ [Range(0f, 1f)]
+ [SerializeField] private float progress = 0f;
+
+ [Header("Smooth Tracking")]
+ [SerializeField] private float lerpSpeed = 4f;
+
+ [Header("Tweak (active in 0.75~1.0 range)")]
+ [Range(0f, 1f)] [SerializeField] private float tweakFuzz = 0f;
+ [Range(0f, 1f)] [SerializeField] private float tweakSpin = 0.5f;
+ [Range(0f, 1f)] [SerializeField] private float tweakHue = 0f;
+
+ [Header("CRT Startup")]
+ [SerializeField] private float startupDuration = 1.8f;
+
+ [Header("Debug")]
+ [SerializeField] [ReadOnlyField] private string currentStage = "";
+ [SerializeField] [ReadOnlyField] private string currentStageName = "";
+ [SerializeField] [ReadOnlyField] private float smoothedProgress;
+
+ private float _currentValue;
+ private float _elapsedTime;
+
+ public float Progress
+ {
+ get => progress;
+ set => progress = Mathf.Clamp01(value);
+ }
+
+ void Update()
+ {
+ if (config == null || waveRenderer == null) return;
+
+ float dt = Time.deltaTime;
+ _elapsedTime += dt;
+
+ // CRT startup ease
+ float startupFactor = Mathf.Clamp01(_elapsedTime / startupDuration);
+ float startupEase = startupFactor * startupFactor;
+
+ // Smooth tracking
+ _currentValue += (progress - _currentValue) * Mathf.Min(1f, dt * lerpSpeed);
+ smoothedProgress = _currentValue;
+
+ // Interpolate wave params
+ var waveParams = config.Interpolate(_currentValue);
+
+ // Inject review parameters based on stage phase
+ string phase = config.GetStagePhase(_currentValue);
+ currentStage = phase;
+ currentStageName = config.GetStageName(_currentValue);
+
+ if (phase == "REVIEW")
+ {
+ float r0 = 0.35f, r1 = 0.5f, r2 = 0.65f;
+ if (_currentValue <= r1)
+ waveParams.reviewIntensity = Smoothstep((_currentValue - r0) / (r1 - r0));
+ else
+ waveParams.reviewIntensity = 0.15f + 0.85f * Smoothstep((r2 - _currentValue) / (r2 - r1));
+ waveParams.featureBoxRate = 3f;
+ waveParams.thresholdLevel = 0.75f;
+ waveParams.cursorSpeed = 0.5f;
+ }
+ else
+ {
+ waveParams.reviewIntensity = 0f;
+ waveParams.featureBoxRate = 0f;
+ waveParams.thresholdLevel = 0.8f;
+ waveParams.cursorSpeed = 0f;
+ }
+
+ // End-stage tweaks (smoothstep fade-in from 0.75)
+ float tweakLinear = Mathf.Clamp01((_currentValue - 0.75f) / 0.1f);
+ float tweakWeight = tweakLinear * tweakLinear * (3f - 2f * tweakLinear);
+ if (tweakWeight > 0.001f)
+ {
+ float fuzz = tweakFuzz * 0.8f;
+ float spin = 0.2f + tweakSpin * 1f;
+ float hueVal = tweakHue * 128f;
+ waveParams.weierstrassAmount = Mathf.Lerp(waveParams.weierstrassAmount, fuzz, tweakWeight);
+ waveParams.phaseSpeed = Mathf.Lerp(waveParams.phaseSpeed, spin, tweakWeight);
+ waveParams.hue = Mathf.Lerp(waveParams.hue, hueVal, tweakWeight);
+ }
+
+ // CRT startup fade
+ waveParams.glowIntensity *= startupEase;
+ waveParams.lightness *= startupEase;
+
+ // Push to renderer
+ waveRenderer.currentParams = waveParams;
+ waveRenderer.time = _elapsedTime;
+ }
+
+ static float Smoothstep(float t)
+ {
+ t = Mathf.Clamp01(t);
+ return t * t * (3f - 2f * t);
+ }
+ }
+
+ ///
+ /// Makes a field read-only in the Inspector.
+ ///
+ public class ReadOnlyFieldAttribute : PropertyAttribute { }
+
+#if UNITY_EDITOR
+ [UnityEditor.CustomPropertyDrawer(typeof(ReadOnlyFieldAttribute))]
+ public class ReadOnlyFieldDrawer : UnityEditor.PropertyDrawer
+ {
+ public override void OnGUI(Rect position, UnityEditor.SerializedProperty property, GUIContent label)
+ {
+ UnityEditor.EditorGUI.BeginDisabledGroup(true);
+ UnityEditor.EditorGUI.PropertyField(position, property, label);
+ UnityEditor.EditorGUI.EndDisabledGroup();
+ }
+ }
+#endif
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveController.cs.meta b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveController.cs.meta
new file mode 100644
index 000000000..ac75d5bb2
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 10373e9ad77e7df4eaf482f687e678d0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveManager.cs b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveManager.cs
new file mode 100644
index 000000000..d47e7077d
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveManager.cs
@@ -0,0 +1,226 @@
+using System.Collections;
+using UnityEngine;
+using DG.Tweening;
+using AibisDream.FixSystem;
+
+namespace AibisDream.MiniGame.HuoShan.EmotionWave
+{
+ ///
+ /// 情绪波形管理器 - 用于控制情绪波形的显示、隐藏和进度动画
+ ///
+ /// 功能:
+ /// - FixSystemCenter 注册/注销
+ /// - DOTween 显示/隐藏动画
+ /// - 进度控制:立即设置 + 平滑动画
+ /// - 平滑追踪:smoothstep lerp 追踪目标进度
+ ///
+ public class EmotionWaveManager : MonoBehaviour
+ {
+ #region 序列化字段
+
+ [Header("References")]
+ [SerializeField] private EmotionWaveController controller;
+ [SerializeField] private Transform containerTransform;
+
+ [Header("Slider Sync")]
+ [Tooltip("可选:关联 SliderController 以自动同步进度")]
+ [SerializeField] private SliderController sliderController;
+ [Tooltip("是否启用滑片同步(勾选后滑片进度会自动驱动情绪波形进度)")]
+ [SerializeField] private bool enableSliderSync = true;
+
+ [Header("动画设置")]
+ [SerializeField] private Vector2 hiddenPosition = new Vector2(0f, -600f);
+ [SerializeField] private Vector2 shownPosition = new Vector2(0f, 0f);
+ [SerializeField] private float showDuration = 0.5f;
+ [SerializeField] private float hideDuration = 0.3f;
+ [SerializeField] private Ease showEase = Ease.OutCubic;
+ [SerializeField] private Ease hideEase = Ease.InCubic;
+
+ [Header("Progress Settings")]
+ [SerializeField] private float lerpSpeed = 4f;
+
+ [Header("Debug")]
+ [SerializeField] [ReadOnly] private float currentProgress;
+ [SerializeField] [ReadOnly] private float targetProgress;
+ [SerializeField] [ReadOnly] private bool isShown;
+
+ #endregion
+
+ #region 私有字段
+
+ private Tweener _progressTweener;
+ private AudioSource _audioSource;
+
+ #endregion
+
+ #region 属性
+
+ public bool IsShown => isShown;
+ public float CurrentProgress => currentProgress;
+ public float TargetProgress => targetProgress;
+
+ #endregion
+
+ #region 生命周期
+
+ private void Awake()
+ {
+ _audioSource = GetComponent();
+ if (_audioSource == null)
+ {
+ _audioSource = gameObject.AddComponent();
+ }
+
+ // 初始化位置为隐藏状态(不控制 scale,由场景/Prefab 决定)
+ if (containerTransform != null)
+ {
+ containerTransform.localPosition = hiddenPosition;
+ }
+
+ // 注册到 FixSystemCenter
+ FixSystemCenter.SystemDic.Register(this);
+
+ // 订阅 SliderController 的进度事件
+ if (sliderController != null && enableSliderSync)
+ {
+ sliderController.onSliderPositionChanged.AddListener(OnSliderPositionChanged);
+ }
+ }
+
+ private void OnDestroy()
+ {
+ // 清理 DOTween
+ _progressTweener?.Kill();
+
+ // 取消订阅
+ if (sliderController != null)
+ {
+ sliderController.onSliderPositionChanged.RemoveListener(OnSliderPositionChanged);
+ }
+
+ // 注销
+ FixSystemCenter.SystemDic.Unregister();
+ }
+
+ private void Update()
+ {
+ if (controller == null) return;
+
+ // smoothstep lerp 追踪目标进度(和 Web 版 lerpSpeed = 4.0 一致)
+ float dt = Time.deltaTime;
+ currentProgress += (targetProgress - currentProgress) * Mathf.Min(1f, dt * lerpSpeed);
+
+ // 将当前进度传递给 Controller
+ controller.Progress = currentProgress;
+ }
+
+ #endregion
+
+ #region 显示/隐藏动画
+
+ ///
+ /// 显示情绪波形(DOTween 动画)
+ ///
+ public IEnumerator Show(float duration = -1f)
+ {
+ if (isShown) yield break;
+
+ float actualDuration = duration > 0 ? duration : showDuration;
+ isShown = true;
+
+ if (containerTransform != null)
+ {
+ yield return containerTransform.DOLocalMove(shownPosition, actualDuration)
+ .SetEase(showEase)
+ .WaitForCompletion();
+ }
+ }
+
+ ///
+ /// 隐藏情绪波形(DOTween 动画)
+ ///
+ public IEnumerator Hide(float duration = -1f)
+ {
+ if (!isShown) yield break;
+
+ float actualDuration = duration > 0 ? duration : hideDuration;
+ isShown = false;
+
+ // 停止所有进度动画
+ _progressTweener?.Kill();
+
+ if (containerTransform != null)
+ {
+ yield return containerTransform.DOLocalMove(hiddenPosition, actualDuration)
+ .SetEase(hideEase)
+ .WaitForCompletion();
+ }
+ }
+
+ #endregion
+
+ #region 进度控制
+
+ ///
+ /// 立即设置进度(不等待)
+ ///
+ public void SetProgress(float value)
+ {
+ _progressTweener?.Kill();
+ targetProgress = Mathf.Clamp01(value);
+ currentProgress = targetProgress;
+ }
+
+ ///
+ /// 动画到指定进度(DOTween 驱动 targetProgress)
+ ///
+ public IEnumerator AnimateToProgress(float target, float duration)
+ {
+ _progressTweener?.Kill();
+
+ target = Mathf.Clamp01(target);
+
+ // 使用 DOTween.To 驱动 targetProgress
+ _progressTweener = DOTween.To(
+ () => targetProgress,
+ x => targetProgress = x,
+ target,
+ duration
+ ).SetEase(Ease.InOutCubic);
+
+ yield return _progressTweener.WaitForCompletion();
+ }
+
+ #endregion
+
+ #region Slider 同步
+
+ ///
+ /// 响应 SliderController 的位置变化事件
+ ///
+ private void OnSliderPositionChanged(float normalizedPosition)
+ {
+ if (!enableSliderSync) return;
+
+ // 滑片进度直接驱动 targetProgress(平滑追踪会在 Update 中处理)
+ targetProgress = Mathf.Clamp01(normalizedPosition);
+ }
+
+ ///
+ /// 启用/禁用滑片同步
+ ///
+ public void SetSliderSyncEnabled(bool enabled)
+ {
+ enableSliderSync = enabled;
+ }
+
+ #endregion
+
+ #region 辅助方法
+
+ [System.Serializable]
+ private class ReadOnlyAttribute : PropertyAttribute { }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveManager.cs.meta b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveManager.cs.meta
new file mode 100644
index 000000000..47ac0aad7
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 55e8be7c583c88f44a97e953a6d806e1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveRenderer.cs b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveRenderer.cs
new file mode 100644
index 000000000..1f604bfbb
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveRenderer.cs
@@ -0,0 +1,805 @@
+using UnityEngine;
+using Shapes;
+
+namespace AibisDream.MiniGame.HuoShan.EmotionWave
+{
+ ///
+ /// Emotion Wave renderer using Shapes immediate mode.
+ /// Ported from Web_EmotionWave/js/renderer.js (OscilloscopeRenderer).
+ ///
+ [ExecuteAlways]
+ public class EmotionWaveRenderer : ImmediateModeShapeDrawer
+ {
+ [Header("Drawing Area")]
+ [Tooltip("绘制区域参考。若挂载了 RectTransform 则用 rect 尺寸转世界坐标,否则用 lossyScale")]
+ [SerializeField] private Transform waveScreen;
+ [Tooltip("波形在绘制区域内的填充比例,1=贴满边线,0.82=默认留边距")]
+ [Range(0.5f, 1f)]
+ [SerializeField] private float fillAmount = 0.82f;
+ [SerializeField] private float thicknessScale = 0.01f;
+
+ [Header("Sampling")]
+ [SerializeField] private int numPoints = 600;
+
+ internal EmotionWaveConfig.WaveParams currentParams;
+ internal float time;
+
+ struct WavePoint
+ {
+ public Vector3 worldPos;
+ public float t;
+ public float tNorm;
+ public float tipIntensity;
+ public float yNorm;
+ public float rawX, rawY;
+ }
+
+ private WavePoint[] _points;
+ private float _scanLineX;
+ private float _cursorX;
+
+ // Blob tracker state
+ private struct BlobTracker
+ {
+ public int pointIndex;
+ public float birth;
+ public float life;
+ }
+ private BlobTracker[] _trackers = new BlobTracker[0];
+ private int _trackerCount;
+ private float _lastTrackerTime;
+
+ // Peak markers
+ private struct PeakMarker
+ {
+ public Vector3 pos;
+ public bool isMax;
+ }
+ private PeakMarker[] _peakMarkers = new PeakMarker[12];
+ private int _peakCount;
+
+ private int _spectrumScanIdx;
+
+ #region Math helpers
+
+ static float Smoothstep(float x)
+ {
+ x = Mathf.Clamp01(x);
+ return x * x * (3f - 2f * x);
+ }
+
+ static float SpikyWave(float t, float spikiness)
+ {
+ float s = Mathf.Sin(t);
+ if (spikiness < 0.01f) return s;
+ float exp = 1f - spikiness * 0.85f;
+ return Mathf.Sign(s) * Mathf.Pow(Mathf.Abs(s) + 0.0001f, exp);
+ }
+
+ static float Waveshape(float x, float gain)
+ {
+ if (gain < 1.05f) return x;
+ float tg = (float)System.Math.Tanh(gain);
+ return (float)System.Math.Tanh(gain * x) / tg;
+ }
+
+ static float PeriodicWave(float phase, float type)
+ {
+ float s = Mathf.Sin(phase);
+ if (type <= 0f) return s;
+ float TWO_PI = Mathf.PI * 2f;
+ float p = (phase / TWO_PI) % 1f;
+ if (p < 0f) p += 1f;
+ float tri = 4f * Mathf.Abs(p - 0.5f) - 1f;
+ float sq = s >= 0f ? 1f : -1f;
+ if (type >= 2f) return sq;
+ if (type >= 1f) return (2f - type) * tri + (type - 1f) * sq;
+ return (1f - type) * s + type * tri;
+ }
+
+ static float Weierstrass(float phase, int layers = 5, float a = 0.55f, float b = 5f)
+ {
+ float sum = 0f;
+ float norm = 0f;
+ for (int k = 0; k <= layers; k++)
+ {
+ float coef = Mathf.Pow(a, k);
+ sum += coef * Mathf.Sin(Mathf.Pow(b, k) * phase);
+ norm += coef;
+ }
+ return norm > 0.001f ? sum / norm : 0f;
+ }
+
+ static float Noise2D(float x, float y)
+ {
+ return Mathf.PerlinNoise(x + 1000f, y + 1000f) * 2f - 1f;
+ }
+
+ #endregion
+
+ void EnsureArrays()
+ {
+ if (_points == null || _points.Length != numPoints + 1)
+ _points = new WavePoint[numPoints + 1];
+ }
+
+ public override void DrawShapes(Camera cam)
+ {
+ if (waveScreen == null) return;
+ EnsureArrays();
+
+ var p = currentParams;
+
+ Vector3 center = waveScreen.position;
+ Vector3 size = waveScreen.lossyScale;
+ float halfW = size.x * 0.5f;
+ float halfH = size.y * 0.5f;
+ float scaleX = halfW * 0.9f;
+ float scaleY = halfH * 0.9f;
+ float z = center.z;
+
+ float dt = Time.deltaTime;
+
+ // Breath
+ float breathPhase = time * p.breathRate * Mathf.PI * 2f;
+ float breath = 1f + Mathf.Sin(breathPhase) * p.breathDepth;
+ float subBreath = 1f + Mathf.Sin(breathPhase * 2.7f + 1.3f) * p.breathDepth * 0.35f;
+ float totalBreath = breath * subBreath;
+
+ // Review pulse
+ float ri = p.reviewIntensity;
+ float reviewPulse = 1f;
+ if (ri > 0.05f)
+ {
+ float pulsePhase = time * 1.5f;
+ reviewPulse = 1f + Mathf.Sin(pulsePhase * Mathf.PI * 2f) * 0.2f * ri;
+ }
+
+ // Scan line position
+ if (ri > 0.05f)
+ _scanLineX = (_scanLineX + dt * 0.3f) % 1f;
+
+ // Cursor position
+ float cursorSpeed = p.cursorSpeed;
+ if (cursorSpeed > 0.001f)
+ _cursorX = (_cursorX + dt * cursorSpeed * 0.25f) % 1f;
+
+ float waveshapeGain = Mathf.Max(1f, p.waveshapeGain);
+ float envelopeDecay = p.envelopeDecay;
+ float spikeIntensity = p.spikeIntensity;
+ float spikeFreq = p.spikeFreq > 0.01f ? p.spikeFreq : 3f;
+ float spikeSharpness = p.spikeSharpness > 0.01f ? p.spikeSharpness : 1f;
+ float weierstrassAmount = p.weierstrassAmount;
+
+ float totalT = Mathf.PI * 2f * Mathf.Max(2, p.numCycles);
+ float waveMode = p.waveMode;
+ float spikiness = p.spikiness;
+ float waveformType = p.waveformType;
+
+ float eased = Smoothstep(waveMode);
+
+ // --- Generate wave points ---
+ for (int i = 0; i <= numPoints; i++)
+ {
+ float tParam = (float)i / numPoints * totalT;
+ float tNorm = (float)i / numPoints;
+
+ // X: sweep + Lissajous oscillation + transition bend
+ float sweepX = (tNorm * 2f - 1f) * p.timeSpread;
+ float lissPhase = p.omegaX * tParam + time * p.phaseSpeed;
+ float blendSpiky = (spikiness > 0.01f) ? Smoothstep(1f - Mathf.Min(1f, waveformType / 0.5f)) : 0f;
+ float spikyX = SpikyWave(lissPhase, spikiness);
+ float periodicX = PeriodicWave(lissPhase, waveformType);
+ float baseX = blendSpiky * spikyX + (1f - blendSpiky) * periodicX;
+ float lissX = p.amplitude * baseX;
+
+ float baseSweep = sweepX * (1f - eased);
+ float oscillation = lissX * eased;
+ float transitionBend = Mathf.Sin(lissPhase) * p.amplitude * 4f * eased * (1f - eased);
+ float x = baseSweep + oscillation + transitionBend;
+
+ // Y: waveform amplitude
+ float yPhase = p.omegaY * tParam + 0.3f;
+ float phaseBlend = Smoothstep((0.25f - eased) / 0.08f);
+ yPhase += time * p.phaseSpeed * phaseBlend;
+ float spikyY = SpikyWave(yPhase, spikiness);
+ float periodicY = PeriodicWave(yPhase, waveformType);
+ float yRaw = blendSpiky * spikyY + (1f - blendSpiky) * periodicY;
+
+ // tanh waveshaping
+ float wsBlend = Smoothstep((waveshapeGain - 1f) / 0.12f);
+ float y = p.amplitudeY * ((1f - wsBlend) * yRaw + wsBlend * Waveshape(yRaw, Mathf.Max(1.01f, waveshapeGain)));
+
+ // Envelope decay
+ if (envelopeDecay > 0.01f)
+ {
+ float envelope = Mathf.Max(0.1f, Mathf.Exp(-tNorm * envelopeDecay * 5f));
+ y *= envelope;
+ x *= Mathf.Max(0.5f, Mathf.Exp(-tNorm * envelopeDecay * 1.5f));
+ }
+
+ // Harmonics
+ if (p.harmonicStrength > 0.001f)
+ {
+ float hf = p.harmonicFreq;
+ float hPhaseX = hf * p.omegaX * tParam + time * p.phaseSpeed * 1.7f;
+ float hPhaseY = hf * p.omegaY * tParam + time * p.phaseSpeed * 0.6f;
+ float hx = p.harmonicStrength * PeriodicWave(hPhaseX, waveformType);
+ float hy = p.harmonicStrength * PeriodicWave(hPhaseY, waveformType);
+ x += hx * eased;
+ y += hy;
+ if (eased < 0.9f) y += hx * (1f - eased) * 0.5f;
+ }
+
+ // Weierstrass fractal
+ if (weierstrassAmount > 0.001f)
+ {
+ int wLayers = Mathf.Clamp(5, 3, 6);
+ float wx = Weierstrass(lissPhase * 2.1f + time * 0.4f, wLayers);
+ float wy = Weierstrass(yPhase * 2.3f + time * 0.3f, wLayers);
+ x += p.amplitude * wx * weierstrassAmount * 0.5f;
+ y += p.amplitudeY * wy * weierstrassAmount * 0.5f;
+ }
+
+ // Noise
+ if (p.noiseAmount > 0.001f)
+ {
+ float ng = 0f;
+ if (waveformType > 0.35f)
+ {
+ float gt = Mathf.Clamp01((waveformType - 0.35f) / 0.25f);
+ ng = gt * gt * (3f - 2f * gt);
+ }
+ else if (waveMode > 0.2f && waveMode < 0.95f)
+ {
+ float raw = (waveMode - 0.2f) * (0.95f - waveMode) * 4.5f;
+ ng = raw * raw * (3f - 2f * Mathf.Min(1f, raw));
+ }
+ if (ng > 0.001f)
+ {
+ float nt = tParam * p.noiseScale * 0.3f;
+ float nTime = time * p.noiseSpeed;
+ float nx = Noise2D(nt, nTime);
+ float ny = Noise2D(nt + 37.7f, nTime + 19.3f);
+ x += nx * p.noiseAmount * ng;
+ y += ny * p.noiseAmount * ng;
+ }
+ }
+
+ // Jitter
+ if (p.jitter > 0.001f)
+ {
+ float jg = 0f;
+ if (waveformType > 0.35f)
+ {
+ float jt = Mathf.Clamp01((waveformType - 0.35f) / 0.25f);
+ jg = jt * jt * (3f - 2f * jt);
+ }
+ else if (waveMode > 0.2f && waveMode < 0.95f)
+ {
+ float rawJ = (waveMode - 0.2f) * (0.95f - waveMode) * 4.5f;
+ jg = rawJ * rawJ * (3f - 2f * Mathf.Min(1f, rawJ));
+ }
+ if (jg > 0.001f)
+ {
+ x += (Random.value - 0.5f) * p.jitter * 2f * jg;
+ y += (Random.value - 0.5f) * p.jitter * 2f * jg;
+ }
+ }
+
+ // Breath
+ x *= totalBreath;
+ y *= totalBreath;
+
+ // Ferrofluid spikes
+ if (spikeIntensity > 0.01f)
+ {
+ float r = Mathf.Sqrt(x * x + y * y);
+ if (r > 0.001f)
+ {
+ float spikeRaw = Mathf.Sin(spikeFreq * tParam + time * 0.5f);
+ float spike = spikeIntensity * Mathf.Pow(Mathf.Max(0f, spikeRaw), spikeSharpness);
+ float rNew = r + spike * 0.3f;
+ x *= rNew / r;
+ y *= rNew / r;
+ }
+ }
+
+ // Tip intensity (red highlight)
+ float tipIntensity = 0f;
+ if (spikeIntensity > 0.2f)
+ {
+ float spikeRawTip = Mathf.Sin(spikeFreq * tParam + time * 0.5f);
+ float spikeValTip = Mathf.Pow(Mathf.Max(0f, spikeRawTip), spikeSharpness);
+ if (spikeValTip > 0.5f)
+ {
+ tipIntensity = (spikeValTip - 0.5f) / 0.5f;
+ tipIntensity *= Mathf.Clamp01((spikeIntensity - 0.2f) / 0.4f);
+ tipIntensity = Mathf.Clamp01(tipIntensity);
+ }
+ }
+
+ float wsTipBlend = waveshapeGain > 1.8f ? Mathf.Clamp01((waveshapeGain - 1.8f) / 0.4f) : 0f;
+ wsTipBlend = wsTipBlend * wsTipBlend * (3f - 2f * wsTipBlend);
+ if (wsTipBlend > 0.001f)
+ {
+ float shapedY = Mathf.Abs(Waveshape(yRaw, waveshapeGain));
+ float wsThreshold = 0.82f;
+ if (shapedY > wsThreshold)
+ {
+ float wsTip = (shapedY - wsThreshold) / (1f - wsThreshold);
+ wsTip *= Mathf.Clamp01((waveshapeGain - 1.8f) / 2.2f);
+ wsTip = Mathf.Min(1f, wsTip * wsTipBlend);
+ tipIntensity = Mathf.Max(tipIntensity, wsTip);
+ }
+ }
+ if (spikiness > 0.2f)
+ {
+ float rawAbsY = Mathf.Abs(PeriodicWave(p.omegaY * tParam + 0.3f, waveformType));
+ float threshold = 0.85f;
+ if (rawAbsY > threshold)
+ {
+ float classicTip = (rawAbsY - threshold) / (1f - threshold);
+ classicTip *= (spikiness - 0.2f) / 0.8f;
+ tipIntensity = Mathf.Max(tipIntensity, Mathf.Clamp01(classicTip));
+ }
+ }
+
+ float yNormVal = y / (p.amplitudeY > 0.001f ? p.amplitudeY : 1f);
+
+ _points[i] = new WavePoint
+ {
+ worldPos = new Vector3(center.x + x * scaleX, center.y + y * scaleY, z),
+ t = tParam,
+ tNorm = tNorm,
+ tipIntensity = tipIntensity,
+ yNorm = yNormVal,
+ rawX = x,
+ rawY = y
+ };
+ }
+
+ // --- Draw ---
+ using (Draw.Command(cam))
+ {
+ Draw.ResetAllDrawStates();
+ Draw.LineGeometry = LineGeometry.Flat2D;
+ Draw.ThicknessSpace = ThicknessSpace.Meters;
+
+ float hue = p.hue;
+ float sat = p.saturation;
+ float lit = p.lightness;
+ float gi = p.glowIntensity * reviewPulse;
+ float frag = p.fragmentation;
+
+ // 6 glow layers (additive)
+ Draw.BlendMode = ShapesBlendMode.Additive;
+ float[] glowWMul = { 10f, 6f, 3.5f, 2f, 1f, 0.5f };
+ float[] glowAMul = { 0.015f, 0.03f, 0.08f, 0.18f, 0.55f, 0.85f };
+
+ for (int la = 0; la < glowWMul.Length; la++)
+ {
+ float alpha = glowAMul[la] * gi;
+ if (alpha < 0.003f) continue;
+
+ float lw = p.lineWidth * glowWMul[la] * thicknessScale;
+ float layerLit = glowWMul[la] > 2f ? Mathf.Min(90f, lit + glowWMul[la] * 3f) : lit;
+ Color layerColor = ColorUtil.HslToRgb(hue, sat, layerLit, alpha);
+
+ Draw.Thickness = lw;
+
+ for (int i = 0; i < numPoints; i++)
+ {
+ // Fragmentation
+ if (frag > 0.01f)
+ {
+ float fragVal = Noise2D(_points[i].t * 1.5f + 5.5f, time * 0.3f + 100f);
+ if (fragVal > (1f - frag * 1.8f)) continue;
+ }
+
+ // Scan line boost
+ Color c = layerColor;
+ if (ri > 0.05f)
+ {
+ float distToScan = Mathf.Abs(_points[i].tNorm - _scanLineX);
+ if (distToScan < 0.03f)
+ {
+ float boost = (1f - distToScan / 0.03f) * 0.8f * ri;
+ c = new Color(
+ Mathf.Clamp01(c.r * (1f + boost)),
+ Mathf.Clamp01(c.g * (1f + boost)),
+ Mathf.Clamp01(c.b * (1f + boost)),
+ Mathf.Clamp01(c.a * (1f + boost * 0.5f))
+ );
+ }
+ }
+
+ Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, c);
+ }
+ }
+
+ // --- Tip layer (red highlights) ---
+ float tipLayer = p.tipLayer;
+ float redTipIntensity = Mathf.Clamp01((tipLayer - 0.4f) / 0.2f);
+ redTipIntensity = redTipIntensity * redTipIntensity * (3f - 2f * redTipIntensity);
+ float redCond = Mathf.Clamp01(
+ Mathf.Max(0f, (spikiness - 0.08f) / 0.2f) +
+ Mathf.Max(0f, (waveshapeGain - 1.5f) / 0.8f) +
+ Mathf.Max(0f, (spikeIntensity - 0.08f) / 0.2f));
+ redTipIntensity *= Mathf.Clamp01(redCond);
+
+ if (redTipIntensity > 0.005f)
+ {
+ float[] tipWMul = { 4f, 2f, 1f, 0.5f };
+ float[] tipAMul = { 0.06f, 0.15f, 0.5f, 0.8f };
+ for (int rl = 0; rl < tipWMul.Length; rl++)
+ {
+ Draw.Thickness = p.lineWidth * tipWMul[rl] * thicknessScale;
+ for (int i = 0; i < numPoints; i++)
+ {
+ float tip = Mathf.Max(_points[i].tipIntensity, _points[i + 1].tipIntensity);
+ if (tip < 0.05f) continue;
+
+ float tipHue = (hue <= 30f || hue >= 330f) ? hue : 128f - tip * 128f;
+ float tipLit = lit + tip * 15f;
+ float tipAlpha = tipAMul[rl] * gi * tip * redTipIntensity;
+ Color tipColor = ColorUtil.HslToRgb(tipHue, sat + tip * 20f, tipLit, tipAlpha);
+
+ Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, tipColor);
+ }
+ }
+ }
+
+ // --- Review overlay ---
+ if (ri > 0.05f)
+ {
+ Draw.BlendMode = ShapesBlendMode.Transparent;
+ DrawReviewOverlay(cam, center, size, scaleY, z, p, ri, hue, sat, lit, gi, dt);
+ }
+
+ // --- Beam spot ---
+ Draw.BlendMode = ShapesBlendMode.Additive;
+ float beamSpeed = p.phaseSpeed * 8f;
+ float beamPhase = (time * beamSpeed) % totalT;
+ int beamIdx = Mathf.FloorToInt((beamPhase / totalT) * numPoints);
+ if (beamIdx >= 0 && beamIdx < _points.Length)
+ {
+ float spotR = p.lineWidth * 2.5f * thicknessScale;
+ float spotLit = Mathf.Min(95f, lit + 35f);
+ Color spotColor = ColorUtil.HslToRgb(hue, Mathf.Max(0f, sat - 15f), spotLit, 0.9f);
+ Draw.Disc(_points[beamIdx].worldPos, spotR, DiscColors.Radial(spotColor, Color.clear));
+ }
+ }
+ }
+
+ void DrawReviewOverlay(Camera cam, Vector3 center, Vector3 size, float scale,
+ float z, EmotionWaveConfig.WaveParams p, float ri,
+ float hue, float sat, float lit, float gi, float dt)
+ {
+ float halfW = size.x * 0.5f;
+ float halfH = size.y * 0.5f;
+ float trackerRate = p.featureBoxRate;
+ float thresholdLevel = p.thresholdLevel;
+ float cursorSpeed = p.cursorSpeed;
+
+ // --- Threshold / Limiter lines ---
+ if (thresholdLevel > 0f && ri > 0.1f)
+ {
+ float threshYTop = center.y + thresholdLevel * p.amplitudeY * scale;
+ float threshYBot = center.y - thresholdLevel * p.amplitudeY * scale;
+ float threshAlpha = 0.3f * ri;
+ Color threshColor = new Color(1f, 0.8f, 0.24f, threshAlpha);
+
+ Draw.Thickness = 0.008f;
+
+ // Dashed lines (simulated)
+ float dashLen = halfW * 0.03f;
+ float gapLen = dashLen * 0.67f;
+ float xStart = center.x - halfW;
+ float xEnd = center.x + halfW;
+ for (float dx = xStart; dx < xEnd; dx += dashLen + gapLen)
+ {
+ float dxEnd = Mathf.Min(dx + dashLen, xEnd);
+ Draw.Line(new Vector3(dx, threshYTop, z), new Vector3(dxEnd, threshYTop, z), threshColor);
+ Draw.Line(new Vector3(dx, threshYBot, z), new Vector3(dxEnd, threshYBot, z), threshColor);
+ }
+
+ // Arrow indicators
+ float arrowSize = 0.02f;
+ Color arrowColor = new Color(1f, 0.8f, 0.24f, threshAlpha * 1.5f);
+ Vector3 arrowLeft = new Vector3(center.x - halfW + 0.01f, 0, z);
+ // Top arrow
+ arrowLeft.y = threshYTop;
+ Draw.Triangle(
+ arrowLeft + new Vector3(0, arrowSize, 0),
+ arrowLeft + new Vector3(arrowSize * 2f, 0, 0),
+ arrowLeft + new Vector3(0, -arrowSize, 0),
+ arrowColor);
+ // Bottom arrow
+ arrowLeft.y = threshYBot;
+ Draw.Triangle(
+ arrowLeft + new Vector3(0, arrowSize, 0),
+ arrowLeft + new Vector3(arrowSize * 2f, 0, 0),
+ arrowLeft + new Vector3(0, -arrowSize, 0),
+ arrowColor);
+
+ // Highlight wave segments exceeding threshold
+ for (int i = 0; i < numPoints; i++)
+ {
+ float py = _points[i].worldPos.y;
+ bool exceeds = py > threshYTop || py < threshYBot;
+ if (!exceeds) continue;
+
+ float exceedAmount;
+ if (py > threshYTop)
+ exceedAmount = (py - threshYTop) / Mathf.Max(0.001f, threshYTop - center.y);
+ else
+ exceedAmount = (threshYBot - py) / Mathf.Max(0.001f, center.y - threshYBot);
+ exceedAmount = Mathf.Clamp01(exceedAmount);
+
+ float exHue = 40f - exceedAmount * 40f;
+ float exAlpha = (0.25f + exceedAmount * 0.45f) * ri;
+ Color exColor = ColorUtil.HslToRgb(exHue, 90f, 55f, exAlpha);
+ Draw.Thickness = p.lineWidth * 1.8f * thicknessScale;
+ Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, exColor);
+ }
+ }
+
+ // --- Scan line (vertical) ---
+ if (ri > 0.1f)
+ {
+ float scanX = center.x - halfW + _scanLineX * size.x;
+ float scanAlpha = 0.25f * ri;
+ Color scanColor = ColorUtil.HslToRgb(128f, 80f, 60f, scanAlpha);
+ Draw.Thickness = 0.005f;
+ Draw.Line(new Vector3(scanX, center.y - halfH, z), new Vector3(scanX, center.y + halfH, z), scanColor);
+
+ // Scan line glow (wider, fainter)
+ Color scanGlow = ColorUtil.HslToRgb(128f, 80f, 60f, scanAlpha * 0.3f);
+ Draw.Thickness = 0.04f;
+ Draw.Line(new Vector3(scanX, center.y - halfH, z), new Vector3(scanX, center.y + halfH, z), scanGlow);
+ }
+
+ // --- Measurement cursor ---
+ if (cursorSpeed > 0.001f && ri > 0.1f)
+ {
+ float cursorScreenX = center.x - halfW + _cursorX * size.x;
+ float cursorAlpha = 0.25f * ri;
+ Color cursorColor = new Color(0.4f, 1f, 0.7f, cursorAlpha);
+
+ // Vertical dashed line
+ Draw.Thickness = 0.004f;
+ float dashLen = halfH * 0.04f;
+ float gapLen = dashLen * 1.5f;
+ for (float dy = center.y - halfH; dy < center.y + halfH; dy += dashLen + gapLen)
+ {
+ float dyEnd = Mathf.Min(dy + dashLen, center.y + halfH);
+ Draw.Line(new Vector3(cursorScreenX, dy, z), new Vector3(cursorScreenX, dyEnd, z), cursorColor);
+ }
+
+ // Find closest wave point
+ int closestIdx = -1;
+ float closestDist = float.MaxValue;
+ for (int i = 0; i <= numPoints; i++)
+ {
+ float dist = Mathf.Abs(_points[i].worldPos.x - cursorScreenX);
+ if (dist < closestDist) { closestDist = dist; closestIdx = i; }
+ }
+
+ if (closestIdx >= 0 && closestDist < size.x * 0.05f)
+ {
+ Vector3 cp = _points[closestIdx].worldPos;
+ float crossSize = 0.02f;
+ Color crossColor = new Color(0.4f, 1f, 0.7f, cursorAlpha * 2.5f);
+ Draw.Thickness = 0.003f;
+ Draw.Line(cp + Vector3.left * crossSize, cp + Vector3.right * crossSize, crossColor);
+ Draw.Line(cp + Vector3.down * crossSize, cp + Vector3.up * crossSize, crossColor);
+ Draw.Disc(cp, 0.006f, crossColor);
+ }
+ }
+
+ // --- Blob Trackers ---
+ if (trackerRate > 0.01f)
+ {
+ float trackerInterval = 1f / trackerRate;
+ if (time - _lastTrackerTime > trackerInterval && _trackerCount < 8)
+ {
+ // Find high-curvature point
+ int bestIdx = numPoints / 2;
+ float bestCurv = 0f;
+ for (int ci = 2; ci < numPoints - 1; ci++)
+ {
+ float curvX = Mathf.Abs(_points[ci - 1].worldPos.x + _points[ci + 1].worldPos.x - 2f * _points[ci].worldPos.x);
+ float curvY = Mathf.Abs(_points[ci - 1].worldPos.y + _points[ci + 1].worldPos.y - 2f * _points[ci].worldPos.y);
+ float curv = curvX + curvY;
+ if (curv > bestCurv) { bestCurv = curv; bestIdx = ci; }
+ }
+
+ if (_trackers.Length < 8) _trackers = new BlobTracker[8];
+ _trackers[_trackerCount] = new BlobTracker
+ {
+ pointIndex = bestIdx,
+ birth = time,
+ life = 1.5f + Random.value
+ };
+ _trackerCount++;
+ _lastTrackerTime = time;
+ }
+
+ // Draw trackers
+ int writeIdx = 0;
+ for (int ti = 0; ti < _trackerCount; ti++)
+ {
+ var tr = _trackers[ti];
+ float age = time - tr.birth;
+ if (age >= tr.life) continue;
+
+ float lifeRatio = age / tr.life;
+ float tAlpha = 1f;
+ if (age < 0.15f) tAlpha = age / 0.15f;
+ else if (lifeRatio > 0.75f) tAlpha = 1f - (lifeRatio - 0.75f) / 0.25f;
+ tAlpha *= ri;
+ tAlpha = Mathf.Clamp01(tAlpha);
+
+ int pIdx = Mathf.Min(tr.pointIndex, numPoints);
+ Vector3 px = _points[pIdx].worldPos;
+ float flashBoost = age < 0.1f ? 1.5f : 1f;
+
+ // Glow disc
+ Color dotGlow = new Color(0f, 1f, 0.47f, 0.15f * tAlpha);
+ Draw.Disc(px, 0.03f, DiscColors.Radial(new Color(0f, 1f, 0.47f, 0.7f * tAlpha * flashBoost), Color.clear));
+
+ // Core dot
+ Color dotCore = new Color(0f, 1f, 0.47f, 0.9f * tAlpha * flashBoost);
+ Draw.Disc(px, 0.008f, dotCore);
+
+ // Leader line
+ float lineDir = px.y < center.y ? -1f : 1f;
+ float lineLen = 0.04f;
+ Color lineColor = new Color(0f, 1f, 0.47f, 0.25f * tAlpha);
+ Draw.Thickness = 0.002f;
+ Draw.Line(px + Vector3.up * lineDir * 0.01f, px + Vector3.up * lineDir * lineLen, lineColor);
+
+ _trackers[writeIdx++] = tr;
+ }
+ _trackerCount = writeIdx;
+ }
+
+ // --- Peak markers ---
+ if (ri > 0.15f)
+ {
+ _peakCount = 0;
+ for (int i = 2; i < numPoints - 1 && _peakCount < 12; i++)
+ {
+ bool isMax = _points[i].worldPos.y > _points[i - 1].worldPos.y && _points[i].worldPos.y > _points[i + 1].worldPos.y;
+ bool isMin = _points[i].worldPos.y < _points[i - 1].worldPos.y && _points[i].worldPos.y < _points[i + 1].worldPos.y;
+ if (!isMax && !isMin) continue;
+
+ float peakDist = Mathf.Abs(_points[i].worldPos.y - center.y);
+ if (peakDist < scale * 0.15f) continue;
+
+ _peakMarkers[_peakCount++] = new PeakMarker { pos = _points[i].worldPos, isMax = isMax };
+ }
+
+ float peakAlpha = 0.35f * ri;
+ Color peakColor = new Color(0f, 1f, 0.47f, peakAlpha);
+ float triSize = 0.012f;
+
+ for (int pi = 0; pi < _peakCount; pi++)
+ {
+ var pk = _peakMarkers[pi];
+ if (pk.isMax)
+ {
+ Draw.Triangle(
+ pk.pos + new Vector3(-triSize, triSize * 2f + 0.005f, 0),
+ pk.pos + new Vector3(triSize, triSize * 2f + 0.005f, 0),
+ pk.pos + new Vector3(0, 0.005f, 0),
+ peakColor);
+ }
+ else
+ {
+ Draw.Triangle(
+ pk.pos + new Vector3(-triSize, -triSize * 2f - 0.005f, 0),
+ pk.pos + new Vector3(triSize, -triSize * 2f - 0.005f, 0),
+ pk.pos + new Vector3(0, -0.005f, 0),
+ peakColor);
+ }
+ }
+ }
+
+ // --- Frequency spectrum bars ---
+ if (ri > 0.05f)
+ DrawFrequencyBars(center, size, z, hue, sat, lit, ri);
+ }
+
+ void DrawFrequencyBars(Vector3 center, Vector3 size, float z,
+ float hue, float sat, float lit, float ri)
+ {
+ int numFreqs = 8;
+ float[] amps = new float[numFreqs];
+
+ int step = Mathf.Max(1, numPoints / 128);
+ int sampleN = numPoints / step;
+
+ for (int f = 0; f < numFreqs; f++)
+ {
+ float cosSum = 0f, sinSum = 0f;
+ int freq = f + 1;
+ for (int si = 0; si < sampleN; si++)
+ {
+ int idx = si * step;
+ if (idx > numPoints) break;
+ float val = _points[idx].rawY;
+ float angle = 2f * Mathf.PI * freq * si / sampleN;
+ cosSum += val * Mathf.Cos(angle);
+ sinSum += val * Mathf.Sin(angle);
+ }
+ cosSum /= sampleN;
+ sinSum /= sampleN;
+ amps[f] = 2f * Mathf.Sqrt(cosSum * cosSum + sinSum * sinSum);
+ }
+
+ float maxAmp = 0f;
+ for (int i = 0; i < numFreqs; i++)
+ if (amps[i] > maxAmp) maxAmp = amps[i];
+ if (maxAmp < 0.001f) return;
+
+ _spectrumScanIdx = Mathf.FloorToInt(time * 1.2f) % numFreqs;
+
+ float halfW = size.x * 0.5f;
+ float halfH = size.y * 0.5f;
+ float chartW = Mathf.Min(halfW * 0.4f, 0.15f);
+ float chartH = Mathf.Min(halfH * 0.3f, 0.06f);
+ float chartX = center.x + halfW - chartW - 0.02f;
+ float chartY = center.y - halfH + 0.02f;
+
+ // Background
+ Color bgColor = new Color(0, 0, 0, 0.5f * ri);
+ Draw.Rectangle(new Vector3(chartX + chartW * 0.5f, chartY + chartH * 0.5f, z),
+ new Vector2(chartW + 0.01f, chartH + 0.01f), bgColor);
+
+ // Border
+ Draw.Thickness = 0.001f;
+ Color borderColor = ColorUtil.HslToRgb(hue, sat, lit, 0.2f * ri);
+ float bx0 = chartX - 0.005f;
+ float by0 = chartY - 0.005f;
+ float bx1 = chartX + chartW + 0.005f;
+ float by1 = chartY + chartH + 0.005f;
+ Draw.Line(new Vector3(bx0, by0, z), new Vector3(bx1, by0, z), borderColor);
+ Draw.Line(new Vector3(bx1, by0, z), new Vector3(bx1, by1, z), borderColor);
+ Draw.Line(new Vector3(bx1, by1, z), new Vector3(bx0, by1, z), borderColor);
+ Draw.Line(new Vector3(bx0, by1, z), new Vector3(bx0, by0, z), borderColor);
+
+ float barGap = 0.002f;
+ float barW = Mathf.Max(0.005f, (chartW - (numFreqs - 1) * barGap) / numFreqs);
+
+ for (int i = 0; i < numFreqs; i++)
+ {
+ float normAmp = amps[i] / maxAmp;
+ float barH = normAmp * chartH * 0.9f;
+ float bx = chartX + i * (barW + barGap);
+ float by = chartY;
+
+ bool isScanned = i == _spectrumScanIdx;
+ float barAlpha = isScanned ? 0.8f * ri : 0.35f * ri;
+ float barLit = isScanned ? lit + 15f : lit;
+
+ Color barColor = ColorUtil.HslToRgb(hue, sat, barLit, barAlpha);
+ Draw.Rectangle(new Vector3(bx + barW * 0.5f, by + barH * 0.5f, z),
+ new Vector2(barW, barH), barColor);
+
+ if (isScanned)
+ {
+ Color markColor = ColorUtil.HslToRgb(hue, sat, lit + 25f, 0.7f * ri);
+ float mx = bx + barW * 0.5f;
+ float my = by + barH + 0.005f;
+ Draw.Triangle(
+ new Vector3(mx - 0.005f, my + 0.008f, z),
+ new Vector3(mx + 0.005f, my + 0.008f, z),
+ new Vector3(mx, my, z),
+ markColor);
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveRenderer.cs.meta b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveRenderer.cs.meta
new file mode 100644
index 000000000..0b7c2f231
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveRenderer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c4c0a33f1933d714aa92cba0bff102bd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveYarnCommand.cs
new file mode 100644
index 000000000..e058079bf
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveYarnCommand.cs
@@ -0,0 +1,159 @@
+using System.Collections;
+using AibisDream.FixSystem;
+using UnityEngine;
+using Yarn.Unity;
+
+namespace AibisDream.MiniGame.HuoShan.EmotionWave
+{
+ ///
+ /// 情绪波形系统的 Yarn 命令接口
+ ///
+ /// 功能:
+ /// - 显示/隐藏:show_emotion_wave, hide_emotion_wave
+ /// - 进度控制:emotion_wave_to(动画到指定进度),set_emotion_wave(立即设置)
+ /// - 状态查询:is_emotion_wave_shown()
+ ///
+ public static class EmotionWaveYarnCommand
+ {
+ private static EmotionWaveManager GetManager()
+ {
+ return FixSystemCenter.SystemDic.Get();
+ }
+
+ #region 显示/隐藏
+
+ ///
+ /// 显示情绪波形
+ /// 用法: <>
+ /// <>
+ ///
+ [YarnCommand("show_emotion_wave")]
+ public static IEnumerator ShowEmotionWave(float duration = -1f)
+ {
+ var manager = GetManager();
+ if (manager != null)
+ {
+ yield return manager.Show(duration);
+ }
+ }
+
+ ///
+ /// 隐藏情绪波形
+ /// 用法: <>
+ /// <>
+ ///
+ [YarnCommand("hide_emotion_wave")]
+ public static IEnumerator HideEmotionWave(float duration = -1f)
+ {
+ var manager = GetManager();
+ if (manager != null)
+ {
+ yield return manager.Hide(duration);
+ }
+ }
+
+ #endregion
+
+ #region 进度控制
+
+ ///
+ /// 动画到指定进度(以指定时间动画到指定进度值,完成后返回给 Yarn)
+ /// 用法: <> -- 2秒内动画到进度0.5
+ /// <> -- 默认1秒内动画到进度1.0
+ ///
+ [YarnCommand("emotion_wave_to")]
+ public static IEnumerator EmotionWaveTo(float progress, float duration = 1f)
+ {
+ var manager = GetManager();
+ if (manager != null)
+ {
+ yield return manager.AnimateToProgress(progress, duration);
+ }
+ }
+
+ ///
+ /// 立即设置进度(不等待)
+ /// 用法: <>
+ ///
+ [YarnCommand("set_emotion_wave")]
+ public static void SetEmotionWave(float progress)
+ {
+ var manager = GetManager();
+ if (manager != null)
+ {
+ manager.SetProgress(progress);
+ }
+ }
+
+ #endregion
+
+ #region 状态查询
+
+ ///
+ /// 检查情绪波形是否已显示
+ /// 用法: <>
+ ///
+ [YarnFunction("is_emotion_wave_shown")]
+ public static bool IsEmotionWaveShown()
+ {
+ var manager = GetManager();
+ return manager != null && manager.IsShown;
+ }
+
+ ///
+ /// 获取当前进度
+ /// 用法: <>
+ ///
+ [YarnFunction("get_emotion_wave_progress")]
+ public static float GetEmotionWaveProgress()
+ {
+ var manager = GetManager();
+ return manager != null ? manager.CurrentProgress : 0f;
+ }
+
+ ///
+ /// 获取目标进度
+ /// 用法: <>
+ ///
+ [YarnFunction("get_emotion_wave_target")]
+ public static float GetEmotionWaveTarget()
+ {
+ var manager = GetManager();
+ return manager != null ? manager.TargetProgress : 0f;
+ }
+
+ #endregion
+
+ #region Slider 同步控制
+
+ ///
+ /// 启用滑片同步(情绪波形进度跟随 SliderController)
+ /// 用法: <>
+ ///
+ [YarnCommand("enable_emotion_wave_slider_sync")]
+ public static void EnableSliderSync()
+ {
+ var manager = GetManager();
+ if (manager != null)
+ {
+ manager.SetSliderSyncEnabled(true);
+ }
+ }
+
+ ///
+ /// 禁用滑片同步(情绪波形进度由 Yarn 命令控制)
+ /// 用法: <>
+ ///
+ [YarnCommand("disable_emotion_wave_slider_sync")]
+ public static void DisableSliderSync()
+ {
+ var manager = GetManager();
+ if (manager != null)
+ {
+ manager.SetSliderSyncEnabled(false);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveYarnCommand.cs.meta b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveYarnCommand.cs.meta
new file mode 100644
index 000000000..aa11b7f00
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveYarnCommand.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1afa5722e9c3ef647b6b4461f2e959f2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/README_SliderSync.md b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/README_SliderSync.md
new file mode 100644
index 000000000..012a76ee2
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/README_SliderSync.md
@@ -0,0 +1,94 @@
+# EmotionWave 与 SliderController 同步使用说明
+
+## 概述
+
+`EmotionWaveManager` 可以自动同步 `SliderController` 的进度,实现滑片位置驱动情绪波形进度的效果。
+
+## 设置方法
+
+### 1. Inspector 配置
+
+在 `EmotionWaveManager` 组件的 Inspector 中:
+
+```
+[Header("Slider Sync")]
+- Slider Controller: 拖入场景中的 SliderController 组件
+- Enable Slider Sync: 勾选启用滑片同步(默认启用)
+```
+
+### 2. 工作原理
+
+当 `Enable Slider Sync` 勾选时:
+- `SliderController` 的 `onSliderPositionChanged` 事件会自动触发
+- `EmotionWaveManager` 接收到事件后更新 `targetProgress`
+- `Update` 中的 smoothstep lerp 会平滑追踪到新进度
+- 最终传递给 `EmotionWaveController` 进行渲染
+
+### 3. Yarn 脚本控制
+
+#### 启用/禁用同步
+
+```yarn
+// 启用滑片同步(情绪波形跟随滑片)
+<>
+
+// 禁用滑片同步(情绪波形由 Yarn 命令独立控制)
+<>
+```
+
+#### 典型使用场景
+
+```yarn
+// 场景1: 显示情绪波形,同时启用滑片同步
+<>
+<>
+
+// 玩家操作滑片时,情绪波形自动跟随
+// ...
+
+// 场景2: 禁用同步,手动控制进度
+<>
+<> // 2秒动画到进度0.5
+
+// 场景3: 立即设置进度(跳过动画)
+<>
+```
+
+## 事件流程
+
+```
+SliderController.SlideToNormalized(0.5f)
+ ↓
+SliderController._currentAngle 更新
+ ↓
+SliderController.onSliderPositionChanged.Invoke(0.5f)
+ ↓
+EmotionWaveManager.OnSliderPositionChanged(0.5f)
+ ↓
+EmotionWaveManager.targetProgress = 0.5f
+ ↓
+EmotionWaveManager.Update() - smoothstep lerp 追踪
+ ↓
+EmotionWaveManager.currentProgress 平滑到 0.5f
+ ↓
+EmotionWaveController.Progress = currentProgress
+ ↓
+EmotionWaveRenderer 渲染对应阶段的波形
+```
+
+## 注意事项
+
+1. **优先级**:当滑片同步启用时,Yarn 命令的 `emotion_wave_to` 和 `set_emotion_wave` 仍然有效,但会与滑片进度产生竞争。建议:
+ - 滑片控制时:启用同步
+ - Yarn 自动演出时:禁用同步
+
+2. **平滑追踪**:无论进度来源(滑片/Yarn),都会经过 smoothstep lerp 平滑处理(lerpSpeed = 4.0)
+
+3. **性能**:事件机制确保只在滑片位置实际改变时触发,不会每帧都更新
+
+## 调试
+
+在 Inspector 的 Debug 区域可以实时查看:
+- `Current Progress`:当前平滑后的进度
+- `Target Progress`:目标进度(来自滑片或 Yarn)
+- `Is Shown`:是否显示状态
diff --git a/Assets/Scripts/MiniGame/HuoShan/EmotionWave/README_SliderSync.md.meta b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/README_SliderSync.md.meta
new file mode 100644
index 000000000..30aeb5a78
--- /dev/null
+++ b/Assets/Scripts/MiniGame/HuoShan/EmotionWave/README_SliderSync.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 028c214d27760e64a9796858c85940fa
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: