using System.Collections; using UnityEngine; using UnityEngine.UI; using WL.Feel; /// /// PD #797 — WLHitFeel 의 코루틴 호스트. WLHitFeel 이 런타임에 1 개만 만들고 /// HideFlags.HideAndDontSave + DontDestroyOnLoad 로 들고 있는다. 씬·프리팹을 건드리지 않는다. /// /// 여기 있는 3 개 코루틴은 전부 **되돌리기 안전** 을 최우선으로 짰다. /// · 히트스톱 : 우리가 넣은 값이 그대로 남아 있을 때만 원래 timeScale 로 복원한다. /// (그 사이 일시정지·서버 팝업·보스 연출이 timeScale 을 바꿨으면 건드리지 않는다) /// · 스케일 펀치: 우리가 쓴 스케일이 그대로 남아 있을 때만 원래 스케일로 복원한다. /// (몹이 풀로 반환·재사용되면 복원을 포기한다 — MobActor.Set() 이 어차피 다시 세팅한다) /// · 플래시 : flashEnabled 가 켜졌을 때만 오버레이 Canvas 를 만든다(드로우 +1). 꺼져 있으면 아무것도 안 만든다. /// public class WLHitFeelRunner : MonoBehaviour { // ── ① 히트스톱 ───────────────────────────────────────────────────────── bool _hitStopActive; float _hitStopPrev = 1f; float _hitStopTarget; public bool HitStopActive { get { return _hitStopActive; } } public void RunHitStop(WLHitFeelSettings st, float duration) { // 이미 진행 중이면 새로 걸지 않는다. 중간에 StopCoroutine 으로 끊으면 // timeScale 이 0 인 채로 영구히 남는 사고가 나므로 '겹치면 무시' 가 유일하게 안전하다. if (_hitStopActive) return; StartCoroutine(CoHitStop(st, duration)); } IEnumerator CoHitStop(WLHitFeelSettings st, float duration) { float prev = Time.timeScale; // 이미 멈춰 있거나(일시정지 0) 느려진 상태면 건드리지 않는다. // Feel 의 MMF_FreezeFrame 도 같은 임계값(MinimumTimescaleThreshold 0.1)을 쓴다. if (prev < st.hitStopMinTimescale) yield break; if (st.hitStopMode == WLHitFeelSettings.eHitStopMode.FeelFreezeFrame) { // Feel 경로 — MMTimeManager 가 timeScale 을 소유하고 해제 시 NormalTimeScale 로 되돌린다. EnsureMMTimeManager(); MoreMountains.Feedbacks.MMFreezeFrameEvent.Trigger(duration); WLHitFeel.LastHitStopMeasured = duration; yield break; } // WLSafe 경로 (기본) _hitStopActive = true; _hitStopPrev = prev; _hitStopTarget = st.hitStopTimeScale; Time.timeScale = _hitStopTarget; float t0 = Time.realtimeSinceStartup; yield return new WaitForSecondsRealtime(duration); // 우리가 넣은 값이 그대로일 때만 복원한다. if (Mathf.Approximately(Time.timeScale, _hitStopTarget)) { Time.timeScale = _hitStopPrev; WLHitFeel.LastHitStopMeasured = Time.realtimeSinceStartup - t0; } else { // 외부(일시정지·서버 팝업·TimeMgr·보스 연출)가 이미 바꿨다 — 덮어쓰지 않는다. WLHitFeel.LastHitStopMeasured = -1f; } _hitStopActive = false; } static void EnsureMMTimeManager() { if (Object.FindAnyObjectByType() != null) return; var go = new GameObject("MMTimeManager"); go.AddComponent(); Object.DontDestroyOnLoad(go); } // ── ③ 피격 몹 스케일 펀치 ─────────────────────────────────────────────── public void RunScalePunch(WLHitFeelSettings st, Actor victim) { if (victim == null) return; StartCoroutine(CoScalePunch(st, victim)); } IEnumerator CoScalePunch(WLHitFeelSettings st, Actor victim) { if (victim == null) yield break; Transform tf = victim.transform; Vector3 baseScale = tf.localScale; float punch = Mathf.Max(1f, st.scalePunch); if (punch <= 1.0001f || baseScale.x <= 0.0001f) yield break; // ProjectileBase.cs:305/308 은 transform.localScale.x > 2f 로 **보스를 판정**하고 // Actor.Get_ScaleSizeDist() 도 같은 임계값을 쓴다. // 펀치가 그 경계를 넘나들면 0.1 초 동안 보스 판정이 흔들리므로 아예 건너뛴다. if (baseScale.x <= 2f && baseScale.x * punch > 2f) yield break; float dur = Mathf.Max(0.01f, st.scalePunchDuration); float t = 0f; Vector3 written = baseScale; while (t < dur) { t += Time.unscaledDeltaTime; if (victim == null || !victim.isActiveAndEnabled) yield break; // 풀 반환 — 복원 포기 // 외부가 스케일을 바꿨다면(리스폰·버프) 우리 것을 덮어쓰지 않는다. if ((tf.localScale - written).sqrMagnitude > 1e-6f) yield break; float k = Mathf.Clamp01(t / dur); float s = Mathf.Sin(k * Mathf.PI); // 0 → 1 → 0 written = baseScale * (1f + (punch - 1f) * s); tf.localScale = written; yield return null; } if (victim != null && victim.isActiveAndEnabled && (tf.localScale - written).sqrMagnitude <= 1e-6f) tf.localScale = baseScale; } // ── ④ 화면 플래시 (기본 OFF) ──────────────────────────────────────────── Canvas _flashCanvas; Image _flashImage; bool _flashActive; public void RunFlash(WLHitFeelSettings st) { if (_flashActive) return; StartCoroutine(CoFlash(st)); } IEnumerator CoFlash(WLHitFeelSettings st) { _flashActive = true; EnsureFlashUI(); float dur = Mathf.Max(0.01f, st.flashDuration); Color c = st.flashColor; float peak = Mathf.Min(c.a, 0.12f); // 발주 규정 상한 float t = 0f; _flashCanvas.enabled = true; while (t < dur) { t += Time.unscaledDeltaTime; float k = Mathf.Clamp01(t / dur); c.a = peak * (1f - k); // 즉시 최대 → 선형 감쇠 _flashImage.color = c; yield return null; } _flashCanvas.enabled = false; // 꺼 두면 드로우콜도 0 _flashActive = false; } void EnsureFlashUI() { if (_flashCanvas != null) return; var go = new GameObject("__WLHitFeelFlash"); go.transform.SetParent(transform, false); _flashCanvas = go.AddComponent(); _flashCanvas.renderMode = RenderMode.ScreenSpaceOverlay; _flashCanvas.sortingOrder = 32000; // 최상단 _flashCanvas.enabled = false; var imgGo = new GameObject("Quad"); imgGo.transform.SetParent(go.transform, false); _flashImage = imgGo.AddComponent(); _flashImage.raycastTarget = false; // 입력을 막지 않는다 _flashImage.color = new Color(1f, 1f, 1f, 0f); var rt = _flashImage.rectTransform; rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; } // ── 안전 원복 ─────────────────────────────────────────────────────────── void OnDisable() { // 히트스톱 도중에 러너가 사라지면 timeScale 이 0 인 채로 남는다 — 반드시 되돌린다. if (_hitStopActive && Mathf.Approximately(Time.timeScale, _hitStopTarget)) Time.timeScale = _hitStopPrev; _hitStopActive = false; } }