using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections; public class ObsessionUI : MonoBehaviour { public static ObsessionUI Instance { get; private set; } [Header("기본 UI 연결")] [SerializeField] private GameObject deathUIPanel; [SerializeField] private Image orbFillImage; [SerializeField] private TextMeshProUGUI xpText; [Header("📝 결과판 텍스트 연결")] [SerializeField] private TextMeshProUGUI timeText; [SerializeField] private TextMeshProUGUI mobText; [SerializeField] private TextMeshProUGUI stageText; [SerializeField] private TextMeshProUGUI bossText; [SerializeField] private TextMeshProUGUI perfectText; [SerializeField] private TextMeshProUGUI deathBonusText; [Header("✨ 정수(에너지) 날아가기 연출")] [SerializeField] private GameObject essencePrefab; // 파란 네모 프리팹 [SerializeField] private float flyDuration = 0.5f; // 날아가는 속도 [Header("💎 보석(레벨업) 연출")] [SerializeField] private GameObject[] gemPrefabs; [SerializeField] private Transform gemContainer; [SerializeField] private float gemRadius = 150f; [Header("연출 설정")] [SerializeField] private float textDelay = 0.1f; [SerializeField] private float waitBeforeFill = 0.6f; [SerializeField] private float fillDuration = 1.5f; private Coroutine _routine; private int[] _xpRequirements; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; } private void Start() { if (deathUIPanel != null) deathUIPanel.SetActive(false); } // 사망 시 호출되는 메인 함수 public void ShowDeathUI(float survivalTime, int mobXP, int stageXP, int bossXP, int perfectXP, int deathBonus, int gainedRunXP, int currentXPBeforeConvert, int[] xpReqs) { if (deathUIPanel == null) return; // 결과창이 뜰 때 게임 시간을 멈춤 (필요에 따라 조절) Time.timeScale = 0f; _xpRequirements = xpReqs; deathUIPanel.SetActive(true); ClearAllTexts(); // 기존 레벨에 맞는 보석 배치 int startingLevelIndex = GetLevelIndex(currentXPBeforeConvert); RefreshGems(startingLevelIndex); if (_routine != null) StopCoroutine(_routine); _routine = StartCoroutine(ShowResultTextRoutine(survivalTime, mobXP, stageXP, bossXP, perfectXP, deathBonus, gainedRunXP, currentXPBeforeConvert)); } private void ClearAllTexts() { if (timeText != null) timeText.text = ""; if (mobText != null) mobText.text = ""; if (stageText != null) stageText.text = ""; if (bossText != null) bossText.text = ""; if (perfectText != null) perfectText.text = ""; if (deathBonusText != null) deathBonusText.text = ""; orbFillImage.fillAmount = 0; xpText.text = ""; } private IEnumerator ShowResultTextRoutine(float survivalTime, int mobXP, int stageXP, int bossXP, int perfectXP, int deathBonus, int gainedRunXP, int currentXPBeforeConvert) { // 1. 기존 경험치 바를 먼저 스르륵 채움 yield return StartCoroutine(AnimateExistingXPRoutine(currentXPBeforeConvert)); // 2. 시간 표시 int minutes = Mathf.FloorToInt(survivalTime / 60f); int seconds = Mathf.FloorToInt(survivalTime % 60f); if (timeText != null) timeText.text = $"생존 시간 : {minutes:00}:{seconds:00}"; yield return new WaitForSecondsRealtime(textDelay); // 3. 각 항목별 텍스트 출력 if (mobText != null) mobText.text = $"몹 처치 : {mobXP} XP"; yield return new WaitForSecondsRealtime(textDelay); if (stageText != null) stageText.text = $"스테이지 클리어 : {stageXP} XP"; yield return new WaitForSecondsRealtime(textDelay); if (bossText != null) bossText.text = $"보스 처치 : {bossXP} XP"; yield return new WaitForSecondsRealtime(textDelay); if (perfectText != null) perfectText.text = $"무피격 클리어 : {perfectXP} XP"; yield return new WaitForSecondsRealtime(textDelay); if (deathBonusText != null) deathBonusText.text = $"사망 보너스 : {deathBonus} XP"; yield return new WaitForSecondsRealtime(waitBeforeFill); // 4. 숫자가 깎이면서 정수가 날아가는 연출 시작 if (mobXP > 0) StartCoroutine(ShootEssencesRoutine(mobText, "몹 처치 : ", mobXP)); if (stageXP > 0) StartCoroutine(ShootEssencesRoutine(stageText, "스테이지 클리어 : ", stageXP)); if (bossXP > 0) StartCoroutine(ShootEssencesRoutine(bossText, "보스 처치 : ", bossXP)); if (perfectXP > 0) StartCoroutine(ShootEssencesRoutine(perfectText, "무피격 클리어 : ", perfectXP)); if (deathBonus > 0) StartCoroutine(ShootEssencesRoutine(deathBonusText, "사망 보너스 : ", deathBonus)); // 정수가 날아가는 시간을 대기 yield return new WaitForSecondsRealtime(flyDuration); // 5. 최종 경험치 바 상승 및 레벨업 체크 yield return StartCoroutine(FillRoutine(gainedRunXP, currentXPBeforeConvert)); } private IEnumerator ShootEssencesRoutine(TextMeshProUGUI textComp, string prefix, int totalXP) { int essenceCount = Mathf.Clamp(totalXP / 5, 1, 10); float totalDrainTime = Mathf.Clamp(totalXP * 0.01f, 0.5f, 1.5f); float essenceInterval = totalDrainTime / essenceCount; float timer = 0f; float nextEssenceTime = 0f; while (timer < totalDrainTime) { timer += Time.unscaledDeltaTime; float percent = timer / totalDrainTime; int currentDisplayXP = Mathf.RoundToInt(Mathf.Lerp(totalXP, 0, percent)); textComp.text = $"{prefix}{currentDisplayXP} XP"; if (timer >= nextEssenceTime && essenceCount > 0) { StartCoroutine(FlyEssenceRoutine(textComp.rectTransform)); nextEssenceTime += essenceInterval; essenceCount--; } yield return null; } textComp.text = $"{prefix}0 XP"; } private IEnumerator FlyEssenceRoutine(RectTransform startUI) { if (essencePrefab == null) yield break; GameObject essence = Instantiate(essencePrefab, deathUIPanel.transform); RectTransform essenceRect = essence.GetComponent(); Vector3 startPos = startUI.position; Vector3 endPos = orbFillImage.transform.position; Vector3 midPoint = (startPos + endPos) / 2f; midPoint.x += Random.Range(-150f, 150f); midPoint.y += Random.Range(50f, 200f); float t = 0; while (t < flyDuration) { t += Time.unscaledDeltaTime; float percent = Mathf.Clamp01(t / flyDuration); float easePercent = percent * percent; Vector3 m1 = Vector3.Lerp(startPos, midPoint, easePercent); Vector3 m2 = Vector3.Lerp(midPoint, endPos, easePercent); if (essenceRect == null) yield break; essenceRect.position = Vector3.Lerp(m1, m2, easePercent); yield return null; } if (essence != null) Destroy(essence); } private IEnumerator FillRoutine(int gainedXP, int startXP) { float t = 0f; int lastLevelIndex = GetLevelIndex(startXP); while (t < fillDuration) { t += Time.unscaledDeltaTime; float p = Mathf.Clamp01(t / fillDuration); int currentAnimatedXP = startXP + Mathf.RoundToInt(gainedXP * p); int shownGainedXP = Mathf.RoundToInt(gainedXP * p); int prevLvlXP, nextLvlXP; int currentLevelIndex = GetLevelIndex(currentAnimatedXP, out prevLvlXP, out nextLvlXP); if (currentLevelIndex > lastLevelIndex) { lastLevelIndex = currentLevelIndex; orbFillImage.fillAmount = 0f; xpText.text = "LEVEL UP!"; RefreshGems(currentLevelIndex); yield return new WaitForSecondsRealtime(0.6f); } if (currentLevelIndex < _xpRequirements.Length) { float range = nextLvlXP - prevLvlXP; float fill = (currentAnimatedXP - prevLvlXP) / range; orbFillImage.fillAmount = fill; } else { orbFillImage.fillAmount = 1f; } xpText.text = $"+ {shownGainedXP} XP"; yield return null; } xpText.text = $"+ {gainedXP} XP"; } private void RefreshGems(int gemCount) { foreach (Transform child in gemContainer) { Destroy(child.gameObject); } if (gemCount <= 0 || gemPrefabs == null || gemPrefabs.Length == 0) return; float angleStep = 360f / gemCount; for (int i = 0; i < gemCount; i++) { float angle = (i * angleStep + 90f) * Mathf.Deg2Rad; float x = Mathf.Cos(angle) * gemRadius; float y = Mathf.Sin(angle) * gemRadius; int prefabIndex = Mathf.Min(i, gemPrefabs.Length - 1); GameObject newGem = Instantiate(gemPrefabs[prefabIndex], gemContainer); newGem.GetComponent().anchoredPosition = new Vector2(x, y); } } private int GetLevelIndex(int xp, out int prevLvlXP, out int nextLvlXP) { prevLvlXP = 0; for (int i = 0; i < _xpRequirements.Length; i++) { if (xp < _xpRequirements[i]) { nextLvlXP = _xpRequirements[i]; return i; } prevLvlXP = _xpRequirements[i]; } nextLvlXP = prevLvlXP; return _xpRequirements.Length; } private int GetLevelIndex(int xp) { int d1, d2; return GetLevelIndex(xp, out d1, out d2); } private IEnumerator AnimateExistingXPRoutine(int currentXPBeforeConvert) { int prevLvlXP, nextLvlXP; GetLevelIndex(currentXPBeforeConvert, out prevLvlXP, out nextLvlXP); float range = Mathf.Max(1, nextLvlXP - prevLvlXP); float targetFill = (float)(currentXPBeforeConvert - prevLvlXP) / range; targetFill = Mathf.Clamp01(targetFill); float t = 0; float setupDuration = 1.0f; while (t < setupDuration) { t += Time.unscaledDeltaTime; orbFillImage.fillAmount = Mathf.Lerp(0f, targetFill, t / setupDuration); yield return null; } orbFillImage.fillAmount = targetFill; } }