// ─────────────────────────────────────────────────────────────────────────────
// LevelUpBurstRunner.cs — LevelUpBurst 의 코루틴 호스트(WLHitFeelRunner 선례)
//
// PD 지시 #813 · 발주서 WL-813k §1-2 (2026-09-09)
//
// LevelUpBurst 가 런타임에 1개만 만들고 DontDestroyOnLoad 로 들고 있는다. 씬·프리팹을 건드리지 않는다.
// 두 코루틴 모두 **되돌리기 안전**을 최우선으로 짰다.
// · 이펙트 파괴: 수명이 끝나면 파괴하고 EffectBudget 슬롯을 정확히 1회만 반납한다(러너가 사라져도 OnDisable 이 반납).
// · 스케일 펀치: 우리가 쓴 스케일이 그대로 남아 있을 때만 원래 스케일로 복원한다(사망·리스폰·버프가 바꿨으면 포기).
// 카메라는 건드리지 않는다(발주서 §1-2 「카메라 미접촉」).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections;
using UnityEngine;
using WL.Combat.Core;
namespace WL.Combat.Growth
{
public class LevelUpBurstRunner : MonoBehaviour
{
/// 반납하지 않은 EffectBudget.Heavy 슬롯 수(러너가 죽을 때 정산한다).
int _heldBudget;
/// 수명이 끝나면 이펙트를 파괴한다. releaseBudget 이면 EffectBudget.Heavy 를 정확히 1회 반납.
public void RunDespawn(GameObject go, float seconds, bool releaseBudget)
{
if (go == null)
{
if (releaseBudget) EffectBudget.Release(EffectBudgetKind.Heavy);
return;
}
if (releaseBudget) _heldBudget++;
StartCoroutine(CoDespawn(go, seconds, releaseBudget));
}
IEnumerator CoDespawn(GameObject go, float seconds, bool releaseBudget)
{
if (seconds > 0f) yield return new WaitForSecondsRealtime(seconds);
if (go != null) Object.Destroy(go);
if (releaseBudget) { _heldBudget--; EffectBudget.Release(EffectBudgetKind.Heavy); }
}
/// PC 스케일 펀치(0 → 최대 → 0 · unscaled). 되돌리기 안전.
public void RunScalePunch(Transform tf, float punch, float duration)
{
if (tf == null) return;
StartCoroutine(CoScalePunch(tf, punch, duration));
}
IEnumerator CoScalePunch(Transform tf, float punch, float duration)
{
if (tf == null) yield break;
Vector3 baseScale = tf.localScale;
if (punch <= 1.0001f || baseScale.x <= 0.0001f) yield break;
// ProjectileBase.cs 는 transform.localScale.x > 2f 로 **보스를 판정**한다.
// 펀치가 그 경계를 넘나들면 잠깐 보스 판정이 흔들리므로 아예 건너뛴다(WLHitFeelRunner 와 같은 규칙).
if (baseScale.x <= 2f && baseScale.x * punch > 2f) yield break;
float dur = Mathf.Max(0.01f, duration);
float t = 0f;
Vector3 written = baseScale;
while (t < dur)
{
t += Time.unscaledDeltaTime;
if (tf == null) 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 (tf != null && (tf.localScale - written).sqrMagnitude <= 1e-6f) tf.localScale = baseScale;
}
void OnDisable()
{
// 러너가 사라지면 잡고 있던 슬롯이 영구히 남는다 — 반드시 반납한다.
while (_heldBudget > 0) { _heldBudget--; EffectBudget.Release(EffectBudgetKind.Heavy); }
}
}
}