236 lines
8.3 KiB
C#

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// 콤보 카운터 UI. 컴팩트 디자인, 오른쪽 수직 중앙.
/// 히트 시 슬라이드 인 + 펀치 스케일, 타임아웃 시 슬라이드 아웃.
/// </summary>
public class ComboCounter : MonoBehaviour
{
#region Fields
[Header("설정")]
[SerializeField] private float comboTimeout = 2f;
[Header("비주얼")]
[SerializeField] private Color comboColor = new Color(1f, 0.85f, 0.15f);
[SerializeField] private Color labelColor = new Color(0.85f, 0.85f, 0.9f);
[SerializeField] private int baseFontSize = 80;
[SerializeField] private int maxFontSize = 120;
[SerializeField] private int fontSizeGrowPerHit = 1;
private Canvas _canvas;
private CanvasGroup _canvasGroup;
private Text _comboText;
private Text _hitLabel;
private RectTransform _rootRect;
private int _currentCombo;
private float _lastHitTime;
private Coroutine _punchCoroutine;
private Coroutine _slideCoroutine;
private Font _font;
private bool _isShowing;
// 슬라이드 위치 (화면 오른쪽 밖 → 안)
private const float OffScreenX = 120f;
private const float OnScreenX = -40f;
#endregion
#region Properties
public int CurrentCombo => _currentCombo;
#endregion
#region Unity Messages
private void Awake()
{
_font = BossRaidFontLoader.Load();
CreateUI();
_canvasGroup.alpha = 0f;
_isShowing = false;
}
private void Update()
{
if (_currentCombo <= 0) return;
if (Time.time - _lastHitTime > comboTimeout)
SlideOut();
}
#endregion
#region Public Methods
public void RegisterHit()
{
_currentCombo++;
_lastHitTime = Time.time;
UpdateDisplay();
// 슬라이드 인 (처음이거나 숨겨진 상태일 때)
if (!_isShowing)
SlideIn();
// 펀치 스케일
if (_punchCoroutine != null) StopCoroutine(_punchCoroutine);
_punchCoroutine = StartCoroutine(PunchCoroutine());
}
public void ResetCombo()
{
_currentCombo = 0;
_isShowing = false;
if (_canvasGroup != null) _canvasGroup.alpha = 0f;
}
public void Hide()
{
ResetCombo();
}
#endregion
#region Private Methods
private void UpdateDisplay()
{
_comboText.text = _currentCombo.ToString();
_comboText.fontSize = Mathf.Min(baseFontSize + _currentCombo * fontSizeGrowPerHit, maxFontSize);
}
private void SlideIn()
{
if (_slideCoroutine != null) StopCoroutine(_slideCoroutine);
_slideCoroutine = StartCoroutine(SlideCoroutine(OffScreenX, OnScreenX, 0f, 1f, 0.2f));
_isShowing = true;
}
private void SlideOut()
{
if (!_isShowing) return;
if (_slideCoroutine != null) StopCoroutine(_slideCoroutine);
_slideCoroutine = StartCoroutine(SlideOutAndReset());
}
private IEnumerator SlideCoroutine(float fromX, float toX, float fromAlpha, float toAlpha, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = elapsed / duration;
float ease = 1f - (1f - t) * (1f - t); // EaseOutQuad
_rootRect.anchoredPosition = new Vector2(Mathf.Lerp(fromX, toX, ease), _rootRect.anchoredPosition.y);
_canvasGroup.alpha = Mathf.Lerp(fromAlpha, toAlpha, ease);
yield return null;
}
_rootRect.anchoredPosition = new Vector2(toX, _rootRect.anchoredPosition.y);
_canvasGroup.alpha = toAlpha;
_slideCoroutine = null;
}
private IEnumerator SlideOutAndReset()
{
yield return SlideCoroutine(OnScreenX, OffScreenX, 1f, 0f, 0.3f);
_currentCombo = 0;
_isShowing = false;
}
private IEnumerator PunchCoroutine()
{
var rect = _comboText.rectTransform;
float punchSize = 1.4f;
float duration = 0.12f;
float elapsed = 0f;
rect.localScale = Vector3.one * punchSize;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = elapsed / duration;
// EaseOutBack
float ease = 1f + 2.7f * Mathf.Pow(t - 1f, 3f) + 1.7f * Mathf.Pow(t - 1f, 2f);
rect.localScale = Vector3.one * Mathf.LerpUnclamped(punchSize, 1f, ease);
yield return null;
}
rect.localScale = Vector3.one;
_punchCoroutine = null;
}
private void CreateUI()
{
var canvasObj = new GameObject("BossRaid_ComboCounter");
canvasObj.transform.SetParent(transform);
_canvas = canvasObj.AddComponent<Canvas>();
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
_canvas.sortingOrder = 101;
_canvasGroup = canvasObj.AddComponent<CanvasGroup>();
_canvasGroup.interactable = false;
_canvasGroup.blocksRaycasts = false;
var scaler = canvasObj.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
// 루트 (오른쪽 수직 중앙, 피벗 오른쪽)
var rootObj = new GameObject("ComboRoot");
rootObj.transform.SetParent(canvasObj.transform, false);
_rootRect = rootObj.AddComponent<RectTransform>();
_rootRect.anchorMin = new Vector2(1f, 0.5f);
_rootRect.anchorMax = new Vector2(1f, 0.5f);
_rootRect.pivot = new Vector2(1f, 0.5f);
_rootRect.sizeDelta = new Vector2(160f, 110f);
_rootRect.anchoredPosition = new Vector2(OffScreenX, 0f);
// 콤보 숫자
var comboObj = new GameObject("ComboNumber");
comboObj.transform.SetParent(rootObj.transform, false);
var comboRect = comboObj.AddComponent<RectTransform>();
comboRect.anchorMin = new Vector2(0f, 0.25f);
comboRect.anchorMax = Vector2.one;
comboRect.offsetMin = Vector2.zero;
comboRect.offsetMax = Vector2.zero;
_comboText = comboObj.AddComponent<Text>();
_comboText.text = "0";
_comboText.fontSize = baseFontSize;
_comboText.color = comboColor;
_comboText.alignment = TextAnchor.MiddleCenter;
_comboText.fontStyle = FontStyle.Bold;
_comboText.horizontalOverflow = HorizontalWrapMode.Overflow;
_comboText.raycastTarget = false;
if (_font != null) _comboText.font = _font;
var outline = comboObj.AddComponent<Outline>();
outline.effectColor = new Color(0.15f, 0.1f, 0f, 1f);
outline.effectDistance = new Vector2(2f, -2f);
// "COMBO" 라벨
var labelObj = new GameObject("ComboLabel");
labelObj.transform.SetParent(rootObj.transform, false);
var labelRect = labelObj.AddComponent<RectTransform>();
labelRect.anchorMin = Vector2.zero;
labelRect.anchorMax = new Vector2(1f, 0.3f);
labelRect.offsetMin = Vector2.zero;
labelRect.offsetMax = Vector2.zero;
_hitLabel = labelObj.AddComponent<Text>();
_hitLabel.text = "COMBO";
_hitLabel.fontSize = 16;
_hitLabel.color = labelColor;
_hitLabel.alignment = TextAnchor.MiddleCenter;
_hitLabel.raycastTarget = false;
if (_font != null) _hitLabel.font = _font;
var labelOutline = labelObj.AddComponent<Outline>();
labelOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
labelOutline.effectDistance = new Vector2(1f, -1f);
}
#endregion
}
}