Project_WL/Assets/WL/Feel/WLHitFeelRunner.cs

204 lines
8.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using WL.Feel;
/// <summary>
/// PD #797 — WLHitFeel 의 코루틴 호스트. WLHitFeel 이 런타임에 1 개만 만들고
/// HideFlags.HideAndDontSave + DontDestroyOnLoad 로 들고 있는다. 씬·프리팹을 건드리지 않는다.
///
/// 여기 있는 3 개 코루틴은 전부 **되돌리기 안전** 을 최우선으로 짰다.
/// · 히트스톱 : 우리가 넣은 값이 그대로 남아 있을 때만 원래 timeScale 로 복원한다.
/// (그 사이 일시정지·서버 팝업·보스 연출이 timeScale 을 바꿨으면 건드리지 않는다)
/// · 스케일 펀치: 우리가 쓴 스케일이 그대로 남아 있을 때만 원래 스케일로 복원한다.
/// (몹이 풀로 반환·재사용되면 복원을 포기한다 — MobActor.Set() 이 어차피 다시 세팅한다)
/// · 플래시 : flashEnabled 가 켜졌을 때만 오버레이 Canvas 를 만든다(드로우 +1). 꺼져 있으면 아무것도 안 만든다.
/// </summary>
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 (WL.Combat.Core.TimeScaleArbiter.TryHandleHitStop(st, duration)) return; // WL #811b: 코어 감속 중재자 경유(코어 비활성이면 false → 아래 기존 경로)
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<MoreMountains.Feedbacks.MMTimeManager>() != null) return;
var go = new GameObject("MMTimeManager");
go.AddComponent<MoreMountains.Feedbacks.MMTimeManager>();
Object.DontDestroyOnLoad(go);
}
// ── ③ 피격 몹 스케일 펀치 ───────────────────────────────────────────────
/// <summary>기존 호출 규약 유지(배수 1 = 설정값 그대로).</summary>
public void RunScalePunch(WLHitFeelSettings st, Actor victim) { RunScalePunch(st, victim, 1f); }
/// <summary>WL #811gh — 티어별 펀치 세기 배수. 실제 배수 = 1 + (scalePunch - 1) × multiplier.</summary>
public void RunScalePunch(WLHitFeelSettings st, Actor victim, float multiplier)
{
if (victim == null) return;
StartCoroutine(CoScalePunch(st, victim, multiplier));
}
IEnumerator CoScalePunch(WLHitFeelSettings st, Actor victim, float multiplier)
{
if (victim == null) yield break;
Transform tf = victim.transform;
Vector3 baseScale = tf.localScale;
// 티어 배수는 '초과분' 에만 곱한다 — 배수 1 이면 기존 값과 완전히 같다(부동소수 오차 0).
float punch = multiplier == 1f
? Mathf.Max(1f, st.scalePunch)
: Mathf.Max(1f, 1f + (Mathf.Max(1f, st.scalePunch) - 1f) * Mathf.Max(0f, multiplier));
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<Canvas>();
_flashCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
_flashCanvas.sortingOrder = 32000; // 최상단
_flashCanvas.enabled = false;
var imgGo = new GameObject("Quad");
imgGo.transform.SetParent(go.transform, false);
_flashImage = imgGo.AddComponent<Image>();
_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;
}
}