using System; using System.Threading; using BriarQueen.Framework.Services.Destruction; using Cysharp.Threading.Tasks; using PrimeTween; using UnityEngine; using UnityEngine.UI; using VContainer; namespace BriarQueen.Game.Effects { [ExecuteAlways] public class UIFogReveal : MonoBehaviour { // ── Shader property IDs ─────────────────────────────────────── private static readonly int _fogAmountId = Shader.PropertyToID("_FogAmount"); private static readonly int _fogColorId = Shader.PropertyToID("_FogColor"); private static readonly int _fogIntensityId = Shader.PropertyToID("_FogIntensity"); private static readonly int _edgeSoftnessId = Shader.PropertyToID("_EdgeSoftness"); private static readonly int _noiseScaleId = Shader.PropertyToID("_NoiseScale"); private static readonly int _driftSpeedId = Shader.PropertyToID("_DriftSpeed"); private static readonly int _densityVariationId = Shader.PropertyToID("_DensityVariation"); private static readonly int _aspectRatioId = Shader.PropertyToID("_AspectRatio"); private static readonly int _fogMotionId = Shader.PropertyToID("_FogMotion"); private static readonly int _useSpriteId = Shader.PropertyToID("_UseSprite"); // ── Inspector ───────────────────────────────────────────────── [Header("References")] [SerializeField] private Image _image; [SerializeField] private Material _fogMaterialTemplate; [Header("Fog Settings")] [SerializeField] private Color _fogColor = new(0.18f, 0.20f, 0.26f, 1f); [SerializeField][Range(0f, 1f)] private float _fogIntensity = 0.95f; [SerializeField][Range(0.05f, 1f)] private float _edgeSoftness = 0.55f; [SerializeField][Range(1f, 20f)] private float _noiseScale = 5f; [SerializeField][Range(0f, 0.5f)] private float _driftSpeed = 0.04f; [SerializeField][Range(0f, 1f)] private float _densityVariation = 0.28f; [SerializeField] private bool _fogMotion = true; [SerializeField] private bool _useSprite = false; [Header("Fog Range")] [SerializeField][Range(0f, 1f)] private float _startFog = 0f; [SerializeField][Range(0f, 1f)] private float _maxFog = 0.6f; [Header("Screen")] [SerializeField] private bool _autoAspectRatio = true; [SerializeField] private float _aspectRatio = 1.777f; [Header("Tween")] [SerializeField] private float _duration = 1.5f; [SerializeField] private Ease _ease = Ease.InOutSine; [SerializeField] private bool _useUnscaledTime = true; [Header("Delay")] [SerializeField][Range(0f, 5f)] private float _fogInDelay = 0f; [SerializeField][Range(0f, 5f)] private float _fogOutDelay = 0f; [Header("Editor Preview")] [SerializeField] private bool _previewInEditMode; [SerializeField][Range(0f, 1f)] private float _previewFogAmount; // ── Runtime state ───────────────────────────────────────────── private Material _runtimeMaterial; private bool _isPreviewMaterial; private Sequence _fogSequence; private CancellationTokenSource _fogCts; private DestructionService _destructionService; // ── Public properties ───────────────────────────────────────── public float FogAmount { get => _runtimeMaterial != null ? _runtimeMaterial.GetFloat(_fogAmountId) : 0f; set => SetFogAmount(value); } public float FogInDuration => _fogInDelay + _duration; public float FogOutDuration => _fogOutDelay + _duration; public bool FogMotion { get => _fogMotion; set { _fogMotion = value; _runtimeMaterial?.SetFloat(_fogMotionId, value ? 1f : 0f); } } public bool UseSprite { get => _useSprite; set { _useSprite = value; _runtimeMaterial?.SetFloat(_useSpriteId, value ? 1f : 0f); } } public float MaxFog { get => _maxFog; set => _maxFog = value; } // ── DI ──────────────────────────────────────────────────────── [Inject] public void Construct(DestructionService destructionService) { _destructionService = destructionService; } // ── Unity lifecycle ─────────────────────────────────────────── private void Awake() { ResolveReferences(); if (Application.isPlaying) { CreateRuntimeMaterial(false); SetFogAmount(_startFog); return; } #if UNITY_EDITOR RefreshEditModePreview(); #endif } private void OnEnable() { #if UNITY_EDITOR if (!Application.isPlaying) RefreshEditModePreview(); #endif } private void OnDisable() { #if UNITY_EDITOR if (!Application.isPlaying) RestoreEditModeMaterial(); #endif } private void Update() { if (!Application.isPlaying) return; if (!_autoAspectRatio) return; var canvas = GetComponentInParent(); if (canvas == null) return; var rt = canvas.GetComponent(); var ratio = rt.rect.width / Mathf.Max(rt.rect.height, 0.001f); _runtimeMaterial?.SetFloat(_aspectRatioId, ratio); } private void OnDestroy() { CancelFog(); if (Application.isPlaying) { DestroyRuntimeMaterial(false); return; } #if UNITY_EDITOR RestoreEditModeMaterial(); #endif } private void OnValidate() { ResolveReferences(); if (Application.isPlaying) { ApplyShaderSettings(); return; } #if UNITY_EDITOR RestoreEditModeMaterial(); RefreshEditModePreview(); #endif } // ── Public API ──────────────────────────────────────────────── /// Animate fog in (startFog → maxFog). Respects _fogInDelay. public UniTask FogIn() => TweenFogWithDelay(_startFog, _maxFog, _duration, _fogInDelay); /// Animate fog in over a custom duration, no delay. public UniTask FogIn(float duration) => TweenFog(_startFog, _maxFog, duration); /// Animate fog out (maxFog → startFog). Respects _fogOutDelay. public UniTask FogOut() => TweenFogWithDelay(_maxFog, _startFog, _duration, _fogOutDelay); /// Animate fog out over a custom duration, no delay. public UniTask FogOut(float duration) => TweenFog(_maxFog, _startFog, duration); /// Animate fog to an arbitrary target amount, no delay. public UniTask FogTo(float target, float duration) => TweenFog(FogAmount, target, duration); /// Snap fog to a value immediately, cancels any running tween. public void FogSet(float amount) { CancelFog(); SetFogAmount(amount); } /// Snap fog to startFog immediately. public void FogReset() { CancelFog(); SetFogAmount(_startFog); } public void CancelFog() { if (_fogSequence.isAlive) { _fogSequence.Stop(); _fogSequence = default; } if (_fogCts != null) { _fogCts.Cancel(); _fogCts.Dispose(); _fogCts = null; } } public async UniTask TweenFog(float from, float to, float duration) { await TweenFogWithDelay(from, to, duration, 0f); } public async UniTask TweenFogWithDelay(float from, float to, float duration, float delay) { if (_runtimeMaterial == null) CreateRuntimeMaterial(false); CancelFog(); _fogCts = new CancellationTokenSource(); SetFogAmount(from); try { if (delay > 0f) { await UniTask.Delay( TimeSpan.FromSeconds(delay), ignoreTimeScale: _useUnscaledTime, cancellationToken: _fogCts.Token); } _fogSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime) .Group(Tween.Custom( from, to, Mathf.Max(0f, duration), SetFogAmount, _ease, useUnscaledTime: _useUnscaledTime)); await _fogSequence.ToUniTask(cancellationToken: _fogCts.Token); SetFogAmount(to); } catch (OperationCanceledException) { // Interrupted — normal, swallow it. } finally { _fogSequence = default; if (_fogCts != null) { _fogCts.Dispose(); _fogCts = null; } } } // ── Internal ────────────────────────────────────────────────── private void SetFogAmount(float amount) { if (_runtimeMaterial == null) return; _runtimeMaterial.SetFloat(_fogAmountId, Mathf.Clamp01(amount)); _image?.SetMaterialDirty(); } private void ApplyShaderSettings() { if (_runtimeMaterial == null) return; _runtimeMaterial.SetColor(_fogColorId, _fogColor); _runtimeMaterial.SetFloat(_fogIntensityId, _fogIntensity); _runtimeMaterial.SetFloat(_edgeSoftnessId, _edgeSoftness); _runtimeMaterial.SetFloat(_noiseScaleId, _noiseScale); _runtimeMaterial.SetFloat(_driftSpeedId, _driftSpeed); _runtimeMaterial.SetFloat(_densityVariationId, _densityVariation); _runtimeMaterial.SetFloat(_fogMotionId, _fogMotion ? 1f : 0f); _runtimeMaterial.SetFloat(_useSpriteId, _useSprite ? 1f : 0f); if (!_autoAspectRatio) _runtimeMaterial.SetFloat(_aspectRatioId, _aspectRatio); _image?.SetMaterialDirty(); } private void ResolveReferences() { if (_image == null) _image = GetComponent(); } private void CreateRuntimeMaterial(bool isPreviewMaterial) { if (_image == null) return; var source = _fogMaterialTemplate != null ? _fogMaterialTemplate : _image.material; if (source == null) return; if (_runtimeMaterial != null) { _image.material = _runtimeMaterial; _image.SetMaterialDirty(); return; } _runtimeMaterial = Instantiate(source); _runtimeMaterial.name = isPreviewMaterial ? $"{nameof(UIFogReveal)} Preview Material" : $"{nameof(UIFogReveal)} Runtime Material"; _isPreviewMaterial = isPreviewMaterial; if (isPreviewMaterial) _runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor; _image.material = _runtimeMaterial; _image.SetMaterialDirty(); ApplyShaderSettings(); } private void DestroyRuntimeMaterial(bool immediate) { if (_runtimeMaterial == null) return; if (_image != null) { _image.material = null; _image.SetMaterialDirty(); } if (immediate) DestroyImmediate(_runtimeMaterial); else Destroy(_runtimeMaterial); _runtimeMaterial = null; _isPreviewMaterial = false; } #if UNITY_EDITOR private void RefreshEditModePreview() { if (Application.isPlaying) return; if (!_previewInEditMode) { RestoreEditModeMaterial(); return; } CreateRuntimeMaterial(true); ApplyShaderSettings(); SetFogAmount(_previewFogAmount); } private void RestoreEditModeMaterial() { if (!_isPreviewMaterial) return; DestroyRuntimeMaterial(true); } #endif } }