using UnityEngine; using UnityEngine.UI; using Platinio.TweenEngine; using VRBeats.ScriptableEvents; namespace VRBeats { public class ScoreManager : MonoBehaviour { private enum BeatJudgement { Perfect, Great, Good, Miss } [SerializeField] private Text multiplierLabel = null; [SerializeField] private Text scoreLabel = null; [SerializeField] private Image multiplierLoader = null; [SerializeField] private float scoreFollowTime = 1.0f; [SerializeField] private CanvasGroup canvasGroup = null; [SerializeField] private GameEvent onGameOver = null; [Header("DJMAX Style Score")] [SerializeField] private Text comboLabel = null; [SerializeField] private Text accuracyLabel = null; [SerializeField] private Text judgementLabel = null; [SerializeField] private bool createMissingHudLabels = true; [SerializeField] private bool applyHudPlacement = true; [SerializeField] private Vector2 hudAnchoredPosition = new Vector2(0.0f, 1.65f); [SerializeField] private float perfectWindow = 0.11f; [SerializeField] private float greatWindow = 0.20f; [SerializeField] private float goodWindow = 0.32f; private int maxMultiplier = 0; private const int MaxCourseScore = 1000000; private const float ProgressBarWidth = 150.0f; private float currentMultiplier = 1.0f; private int acumulateErrors = 0; private int errorLimit = 0; private int totalNoteCount = 0; private int judgedNoteCount = 0; private int currentCombo = 0; private int maxCombo = 0; private int perfectCount = 0; private int greatCount = 0; private int goodCount = 0; private int missCount = 0; private int earnedAccuracyPoints = 0; private float visualScore = 0.0f; private int scoreTweenID = -1; private int loaderTweenID = -1; private BeatJudgement lastJudgement = BeatJudgement.Perfect; private float judgementTimer = 0.0f; private Text progressLabel = null; private Text rankLabel = null; private Image progressBarBackground = null; private Image progressBarFill = null; private Vector3 comboBaseScale = Vector3.one; private float songCurrentTime = 0.0f; private float songDuration = 0.0f; private bool resultFinalized = false; private bool destroyed = false; private static bool hasPendingSliceTiming = false; private static float pendingSliceTiming = 0.0f; private static Font hudFont = null; private Image ringBackground = null; public int CurrentScore { get { float accuracyRatio = AccuracyPercent / 100.0f; float comboRatio = totalNoteCount > 0 ? maxCombo / (float)totalNoteCount : 0.0f; return Mathf.RoundToInt(800000.0f * accuracyRatio + 200000.0f * comboRatio); } } public float AccuracyPercent { get { int denominatorNotes = totalNoteCount > 0 ? totalNoteCount : judgedNoteCount; if (denominatorNotes <= 0) return 100.0f; return (float)earnedAccuracyPoints / (denominatorNotes * 1000) * 100.0f; } } public string Rank { get { if (CurrentScore >= MaxCourseScore) return "M"; float accuracy = AccuracyPercent; if (accuracy >= 98.0f) return "S+"; if (accuracy >= 95.0f) return "S"; if (accuracy >= 90.0f) return "A"; if (accuracy >= 80.0f) return "B"; if (accuracy >= 70.0f) return "C"; if (accuracy >= 60.0f) return "D"; return "F"; } } public int MaxCombo => maxCombo; public string RankColorHex => GetRankColorHex(); private void Awake() { maxMultiplier = VR_BeatManager.instance.GameSettings.MaxMultiplier; errorLimit = VR_BeatManager.instance.GameSettings.ErrorLimit; if (multiplierLoader != null) multiplierLoader.fillAmount = 0.0f; canvasGroup ??= GetComponent() ?? gameObject.AddComponent(); PrepareHud(); } public static void ReportSliceTiming(float timingErrorSeconds) { pendingSliceTiming = timingErrorSeconds; hasPendingSliceTiming = true; } public static void ReportMiss() { hasPendingSliceTiming = false; pendingSliceTiming = 0.0f; } public void SetTotalNotes(int noteCount) { totalNoteCount = Mathf.Max(0, noteCount); resultFinalized = false; UpdateScoreTween(); } public void CompleteSong() { if (resultFinalized) return; resultFinalized = true; int missedUnjudgedNotes = Mathf.Max(0, totalNoteCount - judgedNoteCount); if (missedUnjudgedNotes > 0) { missCount += missedUnjudgedNotes; judgedNoteCount += missedUnjudgedNotes; currentCombo = 0; currentMultiplier = 1.0f; } UpdateScoreTween(); } public void SetSongProgress(float currentTime, float duration) { songDuration = Mathf.Max(0.0f, duration); songCurrentTime = songDuration > 0.0f ? Mathf.Clamp(currentTime, 0.0f, songDuration) : Mathf.Max(0.0f, currentTime); } public void OnGameOver() { gameObject.CancelAllTweens(); if (canvasGroup != null) { canvasGroup.alpha = 0.0f; canvasGroup.interactable = false; canvasGroup.blocksRaycasts = false; } SetSaberVisibility(false); } public void OnGameRestart() { ResetThisComponent(); gameObject.CancelAllTweens(); if (canvasGroup != null) { canvasGroup.alpha = 1.0f; canvasGroup.interactable = true; canvasGroup.blocksRaycasts = false; } SetSaberVisibility(true); } public void ResetThisComponent() { currentMultiplier = 1.0f; visualScore = 0; acumulateErrors = 0; judgedNoteCount = 0; currentCombo = 0; maxCombo = 0; perfectCount = 0; greatCount = 0; goodCount = 0; missCount = 0; earnedAccuracyPoints = 0; judgementTimer = 0.0f; resultFinalized = false; hasPendingSliceTiming = false; pendingSliceTiming = 0.0f; if (multiplierLoader != null) multiplierLoader.fillAmount = 0.0f; } private void Update() { UpdateUI(); } public void OnCorrectSlice() { if (destroyed) return; BeatJudgement judgement = ConsumeJudgement(); RegisterJudgement(judgement); acumulateErrors = judgement == BeatJudgement.Miss ? acumulateErrors + 1 : 0; UpdateScoreTween(); UpdateMultiplierLoaderValue(); } public void OnIncorrectSlice() { if (destroyed) return; RegisterJudgement(BeatJudgement.Miss); acumulateErrors++; currentMultiplier = 1.0f; UpdateScoreTween(); UpdateMultiplierLoaderValue(); if (acumulateErrors > errorLimit) onGameOver.Invoke(); } public string BuildResultSummary(int minScoreLength) { string score = CurrentScore.ToString("N0"); return $"{Rank}" + $"{score}\n" + $"MAX COMBO {maxCombo}"; } private void CancelTweenById(int id) { if (id != -1) PlatinioTween.instance.CancelTween(id); } private void UpdateMultiplierLoaderValue() { if (destroyed || multiplierLoader == null) return; float multiplierLoaderValue = GetComboTierProgress(); CancelTweenById(loaderTweenID); loaderTweenID = PlatinioTween.instance.ValueTween(multiplierLoader.fillAmount, multiplierLoaderValue, 1.0f) .SetEase(Ease.EaseOutExpo) .SetOnUpdateFloat(delegate (float value) { if (multiplierLoader != null) multiplierLoader.fillAmount = value; }) .SetOwner(multiplierLoader.gameObject) .ID; } private void UpdateScoreTween() { CancelTweenById(scoreTweenID); scoreTweenID = PlatinioTween.instance.ValueTween(visualScore, CurrentScore, scoreFollowTime) .SetEase(Ease.EaseOutExpo) .SetOnUpdateFloat(delegate (float value) { visualScore = value; }) .ID; } private void UpdateUI() { if (destroyed) return; if (multiplierLabel != null) multiplierLabel.text = $"x{Mathf.RoundToInt(currentMultiplier)}"; if (scoreLabel != null) scoreLabel.text = $"{Mathf.CeilToInt(visualScore):N0}"; if (comboLabel != null) comboLabel.text = currentCombo > 0 ? $"COMBO\n{currentCombo}" : "COMBO\n0"; if (accuracyLabel != null) accuracyLabel.text = $"{AccuracyPercent:0.0}%"; if (rankLabel != null) rankLabel.text = $"{Rank}"; if (progressLabel != null) progressLabel.text = songDuration > 0.0f ? $"{FormatTime(songCurrentTime)} / {FormatTime(songDuration)}" : ""; if (progressBarFill != null) SetProgressBarFill(songDuration > 0.0f ? Mathf.Clamp01(songCurrentTime / songDuration) : 0.0f); if (judgementLabel == null) return; judgementTimer -= Time.deltaTime; judgementLabel.text = judgementTimer > 0.0f ? GetJudgementText(lastJudgement) : ""; judgementLabel.color = GetJudgementColor(lastJudgement); } private BeatJudgement ConsumeJudgement() { if (!hasPendingSliceTiming) return BeatJudgement.Perfect; float timing = pendingSliceTiming; hasPendingSliceTiming = false; pendingSliceTiming = 0.0f; if (timing <= perfectWindow) return BeatJudgement.Perfect; if (timing <= greatWindow) return BeatJudgement.Great; if (timing <= goodWindow) return BeatJudgement.Good; return BeatJudgement.Good; } private void RegisterJudgement(BeatJudgement judgement) { lastJudgement = judgement; judgementTimer = 0.45f; judgedNoteCount++; if (judgement == BeatJudgement.Perfect) { perfectCount++; earnedAccuracyPoints += 1000; currentCombo++; } else if (judgement == BeatJudgement.Great) { greatCount++; earnedAccuracyPoints += 900; currentCombo++; } else if (judgement == BeatJudgement.Good) { goodCount++; earnedAccuracyPoints += 700; currentCombo++; } else { missCount++; currentCombo = 0; } maxCombo = Mathf.Max(maxCombo, currentCombo); currentMultiplier = judgement == BeatJudgement.Miss ? 1.0f : Mathf.Min(GetComboMultiplier(currentCombo), Mathf.Max(1.0f, maxMultiplier)); PulseComboLabel(judgement); } private static float GetComboMultiplier(int combo) { if (combo >= 50) return 1.5f; if (combo >= 30) return 1.35f; if (combo >= 15) return 1.2f; if (combo >= 5) return 1.1f; return 1.0f; } private static string GetJudgementText(BeatJudgement judgement) { switch (judgement) { case BeatJudgement.Perfect: return "PERFECT"; case BeatJudgement.Great: return "GREAT"; case BeatJudgement.Good: return "GOOD"; default: return "BREAK"; } } private static Color GetJudgementColor(BeatJudgement judgement) { switch (judgement) { case BeatJudgement.Perfect: return new Color(0.25f, 0.95f, 1.0f, 1.0f); case BeatJudgement.Great: return new Color(0.58f, 1.0f, 0.45f, 1.0f); case BeatJudgement.Good: return new Color(1.0f, 0.8f, 0.35f, 1.0f); default: return new Color(1.0f, 0.25f, 0.45f, 1.0f); } } private float GetComboTierProgress() { int lower = 0; int upper = 5; if (currentCombo >= 50) return 1.0f; if (currentCombo >= 30) { lower = 30; upper = 50; } else if (currentCombo >= 15) { lower = 15; upper = 30; } else if (currentCombo >= 5) { lower = 5; upper = 15; } return Mathf.InverseLerp(lower, upper, currentCombo); } private void PrepareHud() { RectTransform rect = transform as RectTransform; if (applyHudPlacement && rect != null) rect.anchoredPosition = hudAnchoredPosition; comboLabel = comboLabel != null ? comboLabel : FindHudText("Combo"); accuracyLabel = accuracyLabel != null ? accuracyLabel : FindHudText("Accuracy"); rankLabel = rankLabel != null ? rankLabel : FindHudText("Rank"); judgementLabel = judgementLabel != null ? judgementLabel : FindHudText("Judgement"); progressLabel = progressLabel != null ? progressLabel : FindHudText("SongProgress"); progressBarBackground = progressBarBackground != null ? progressBarBackground : FindHudImage("SongProgressBarBackground"); progressBarFill = progressBarFill != null ? progressBarFill : FindHudImage("SongProgressBarFill"); if (!createMissingHudLabels) return; comboLabel ??= CreateHudText("Combo"); accuracyLabel ??= CreateHudText("Accuracy"); rankLabel ??= CreateHudText("Rank"); judgementLabel ??= CreateHudText("Judgement"); progressLabel ??= CreateHudText("SongProgress"); progressBarBackground ??= CreateHudImage("SongProgressBarBackground"); progressBarFill ??= CreateHudImage("SongProgressBarFill"); ringBackground = ringBackground != null ? ringBackground : FindHudImage("MultiplierRingBg"); ringBackground ??= CreateHudImage("MultiplierRingBg"); ConfigureText(comboLabel, new Vector2(-335.0f, 92.0f), new Vector2(190.0f, 118.0f), 34, Color.white, TextAnchor.MiddleCenter); ConfigureText(scoreLabel, new Vector2(-335.0f, 10.0f), new Vector2(190.0f, 42.0f), 22, Color.white, TextAnchor.MiddleCenter); ConfigureText(accuracyLabel, new Vector2(-335.0f, -34.0f), new Vector2(190.0f, 32.0f), 18, new Color(0.84f, 0.94f, 1.0f, 1.0f), TextAnchor.MiddleCenter); ConfigureText(rankLabel, new Vector2(-335.0f, -92.0f), new Vector2(190.0f, 72.0f), 48, Color.white, TextAnchor.MiddleCenter); ConfigureText(judgementLabel, new Vector2(0.0f, 118.0f), new Vector2(280.0f, 56.0f), 28, new Color(0.25f, 0.95f, 1.0f, 1.0f), TextAnchor.MiddleCenter); ConfigureText(multiplierLabel, new Vector2(335.0f, 38.0f), new Vector2(118.0f, 76.0f), 34, Color.white, TextAnchor.MiddleCenter); ConfigureText(progressLabel, new Vector2(335.0f, -75.0f), new Vector2(180.0f, 30.0f), 17, Color.white, TextAnchor.MiddleCenter); ConfigureImage(multiplierLoader, new Vector2(335.0f, 38.0f), new Vector2(112.0f, 112.0f), new Color(1.0f, 1.0f, 1.0f, 0.78f)); ConfigureImage(ringBackground, new Vector2(335.0f, 38.0f), new Vector2(112.0f, 112.0f), new Color(1.0f, 1.0f, 1.0f, 0.15f)); if (ringBackground != null && multiplierLoader != null) { ringBackground.sprite = multiplierLoader.sprite; ringBackground.type = Image.Type.Simple; ringBackground.transform.SetSiblingIndex(multiplierLoader.transform.GetSiblingIndex()); } ConfigureImage(progressBarBackground, new Vector2(335.0f, -48.0f), new Vector2(ProgressBarWidth, 5.0f), new Color(1.0f, 1.0f, 1.0f, 0.22f)); ConfigureImage(progressBarFill, new Vector2(335.0f - ProgressBarWidth * 0.5f, -48.0f), new Vector2(ProgressBarWidth, 5.0f), Color.white); if (progressBarFill != null) { RectTransform fillRect = progressBarFill.rectTransform; fillRect.pivot = new Vector2(0.0f, 0.5f); SetProgressBarFill(0.0f); } comboBaseScale = comboLabel != null ? comboLabel.transform.localScale : Vector3.one; } private Text CreateHudText(string name) { GameObject textObject = new GameObject(name); textObject.layer = gameObject.layer; textObject.transform.SetParent(transform, false); RectTransform rect = textObject.AddComponent(); textObject.AddComponent(); Text text = textObject.AddComponent(); return text; } private Image CreateHudImage(string name) { GameObject imageObject = new GameObject(name); imageObject.layer = gameObject.layer; imageObject.transform.SetParent(transform, false); imageObject.AddComponent(); return imageObject.AddComponent(); } private static void ConfigureText(Text text, Vector2 anchoredPosition, Vector2 size, int fontSize, Color color, TextAnchor alignment) { if (text == null) return; if (text.font == null) text.font = HudFont; RectTransform rect = text.rectTransform; rect.anchorMin = new Vector2(0.5f, 0.5f); rect.anchorMax = new Vector2(0.5f, 0.5f); rect.anchoredPosition = anchoredPosition; rect.sizeDelta = size; text.fontSize = fontSize; text.color = color; text.alignment = alignment; text.horizontalOverflow = HorizontalWrapMode.Overflow; text.verticalOverflow = VerticalWrapMode.Overflow; text.raycastTarget = false; text.supportRichText = true; text.lineSpacing = 0.86f; Shadow shadow = text.GetComponent() ?? text.gameObject.AddComponent(); shadow.effectColor = new Color(0.0f, 0.0f, 0.0f, 0.72f); shadow.effectDistance = new Vector2(3.0f, -3.0f); } private static void ConfigureImage(Image image, Vector2 anchoredPosition, Vector2 size) { ConfigureImage(image, anchoredPosition, size, new Color(1.0f, 1.0f, 1.0f, 0.85f)); } private static void ConfigureImage(Image image, Vector2 anchoredPosition, Vector2 size, Color color) { if (image == null) return; RectTransform rect = image.rectTransform; rect.anchorMin = new Vector2(0.5f, 0.5f); rect.anchorMax = new Vector2(0.5f, 0.5f); rect.anchoredPosition = anchoredPosition; rect.sizeDelta = size; image.color = color; image.raycastTarget = false; } private void SetProgressBarFill(float progress) { if (progressBarFill == null) return; RectTransform rect = progressBarFill.rectTransform; rect.sizeDelta = new Vector2(ProgressBarWidth * Mathf.Clamp01(progress), rect.sizeDelta.y); } private static Font HudFont { get { if (hudFont == null) hudFont = Resources.GetBuiltinResource("LegacyRuntime.ttf"); return hudFont; } } private Text FindHudText(string objectName) { Transform child = transform.Find(objectName); return child != null ? child.GetComponent() : null; } private Image FindHudImage(string objectName) { Transform child = transform.Find(objectName); return child != null ? child.GetComponent() : null; } private string GetRankColorHex() { switch (Rank) { case "M": return "#E8B7FF"; case "S+": return "#41F2FF"; case "S": return "#FFD95C"; case "A": return "#B9FF72"; case "B": return "#FFE06A"; case "C": return "#FFB15C"; case "D": return "#FF7C7C"; default: return "#A9B7C0"; } } private void PulseComboLabel(BeatJudgement judgement) { if (comboLabel == null || judgement == BeatJudgement.Miss) return; comboLabel.gameObject.CancelAllTweens(); comboLabel.transform.localScale = comboBaseScale * 1.08f; comboLabel.transform.ScaleTween(comboBaseScale, 0.16f).SetEase(Ease.EaseOutExpo).SetOwner(comboLabel.gameObject); } private static string FormatTime(float seconds) { int wholeSeconds = Mathf.Max(0, Mathf.FloorToInt(seconds)); int minutes = wholeSeconds / 60; int remainingSeconds = wholeSeconds % 60; return $"{minutes}:{remainingSeconds:00}"; } private static void SetSaberVisibility(bool visible) { VR_Saber[] sabers = FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < sabers.Length; i++) { if (sabers[i] == null) continue; if (visible) sabers[i].MakeVisible(); else sabers[i].MakeInvisible(); } } } }