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: