275 lines
10 KiB
C#
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;
|
|||
|
|
}
|
|||
|
|
}
|