Projext/Assets/Scripts/Obsession/ObsessionUI.cs
2026-02-22 22:37:34 +09:00

275 lines
10 KiB
C#

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<RectTransform>();
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 = "<color=#00FF00>LEVEL UP!</color>";
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<RectTransform>().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;
}
}