Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Assets/Script/Gameplay/GameManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,9 @@ private bool EndSong()
0 :
// PendingScore should be 0 at this point, so no reason to add it
(float) player.BaseStats.CommittedScore / player.BaseEngine.BaseNoteScore,
VocalPhrasePercents = (player as VocalsPlayer)?.PhrasePercents,
VocalPercussionHits = (player as VocalsPlayer)?.PercussionHits ?? 0,
VocalPercussionTotal = (player as VocalsPlayer)?.PercussionTotal ?? 0,
}).ToArray(),
BandScore = BandScore,
BandStars = (int) BandStars,
Expand Down
14 changes: 4 additions & 10 deletions Assets/Script/Gameplay/HUD/VocalsPlayerHUD.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using UnityEngine;
using UnityEngine.UI;
using YARG.Core.Game;
using YARG.Gameplay.Vocals;
using YARG.Helpers.Extensions;
using YARG.Helpers.UI;
using YARG.Localization;
Expand Down Expand Up @@ -108,16 +109,9 @@ public void UpdateInfo(float phrasePercent, int multiplier,

public static string GetVocalPerformanceText(double hitPercent)
{
string performanceKey = hitPercent switch
{
>= 1f => "Awesome",
>= 0.8f => "Strong",
>= 0.7f => "Good",
>= 0.6f => "Okay",
>= 0.1f => "Messy",
_ => "Awful"
};

// Single source of truth for the grade thresholds lives in VocalPhraseGrade.Classify,
// shared with the score-screen phrase summary so the two can't drift.
var performanceKey = VocalPhraseGradeExtensions.Classify(hitPercent).ToLocalizationKey();
return Localize.Key("Gameplay.Vocals.Performance", performanceKey);
}

Expand Down
46 changes: 45 additions & 1 deletion Assets/Script/Gameplay/Player/VocalsPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ public class VocalsPlayer : BasePlayer

private int _phraseIndex = -1;

// Per-phrase normalized hit percents, captured live from OnPhraseHit in chronological
// (song) order. Routed to the score screen's Advanced view for the vocals phrase summary.
private readonly List<float> _phrasePercents = new();
public IReadOnlyList<float> PhrasePercents => _phrasePercents;

// Vocal percussion isn't tracked in stats; tally hits/total live for the score screen.
// Total is derived from chart data so it's correct regardless of shared mutable state
// (multiple engines on the same VocalsPart share VocalNote objects — first engine to hit
// a percussion note mutates WasHit, causing sibling engines to skip it).
private int _percussionHits;
private int _percussionTotalFromChart;
public int PercussionHits => _percussionHits;
public int PercussionTotal => _percussionTotalFromChart;

private const int NEEDLES_COUNT = 7;

private SongChart _chart;
Expand Down Expand Up @@ -95,6 +109,11 @@ public void Initialize(int index, int vocalIndex, YargPlayer player, SongChart c
OriginalNoteTrack = track.CloneAsInstrumentDifficulty();
NoteTrack = OriginalNoteTrack;

// Count percussion notes from the chart data so the denominator is authoritative
// regardless of shared-mutable-state bugs between engines on the same part.
_percussionTotalFromChart = NoteTrack.Notes
.Sum(phrase => phrase.ChildNotes.Count(n => n.IsPercussion));

_phraseIndex = -1;
_previousStarPowerPercent = 0.0;

Expand Down Expand Up @@ -189,6 +208,10 @@ protected VocalsEngine CreateEngine()

LastCombo = Combo;

// Capture the phrase score for the score screen's phrase summary. The event fires
// once per non-empty phrase in chronological order, so this list stays in song order.
_phrasePercents.Add((float) percent);

ShowTextNotifications(isLastPhrase);

// Order is important here. ShowVocalPhraseResult() will skip showing AWESOME! if other, more important notifications are already showing.
Expand All @@ -200,10 +223,11 @@ protected VocalsEngine CreateEngine()
if (note.IsPercussion)
{
_percussionTrack.HitPercussionNote(note);
_percussionHits++;
}
};

engine.OnNoteMissed += (_, _) =>
engine.OnNoteMissed += (_, note) =>
{
if (LastCombo >= 2)
{
Expand Down Expand Up @@ -252,8 +276,28 @@ protected override void ResetVisuals()
_lastTargetNote = null;
}

// The phrase/percussion captures live Unity-side (the engine doesn't store them), so they
// must be cleared by hand whenever the engine resets and reprocesses — otherwise the
// re-fired OnPhraseHit/OnNoteHit events would append duplicates (e.g. on replay seek).
private void ResetScoreScreenCaptures()
{
_phrasePercents.Clear();
_percussionHits = 0;
}

public override void SetReplayTime(double time)
{
// Base resets the engine and reprocesses inputs up to `time`; clear first so the
// re-fired events rebuild the captures from scratch.
ResetScoreScreenCaptures();

base.SetReplayTime(time);
}

public override void ResetPracticeSection()
{
ResetScoreScreenCaptures();

Engine.Reset(true);

if (NoteTrack.Notes.Count > 0)
Expand Down
8 changes: 8 additions & 0 deletions Assets/Script/Gameplay/Vocals.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions Assets/Script/Gameplay/Vocals/VocalPhraseGrade.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace YARG.Gameplay.Vocals
{
// Ordered worst-to-best so the ordinal can be used directly for sorting tallies.
public enum VocalPhraseGrade
{
Awful,
Messy,
Okay,
Good,
Strong,
Awesome
}

public static class VocalPhraseGradeExtensions
{
// Inclusive lower bound per tier, indexed by (int) VocalPhraseGrade (worst -> best).
private static readonly double[] LowerBounds = { 0.0, 0.1, 0.6, 0.7, 0.8, 1.0 };

public static VocalPhraseGrade Classify(double normalizedPercent)
{
for (int i = LowerBounds.Length - 1; i >= 0; i--)
{
if (normalizedPercent >= LowerBounds[i])
{
return (VocalPhraseGrade) i;
}
}

return VocalPhraseGrade.Awful;
}

public static double LowerBound(this VocalPhraseGrade grade)
{
return LowerBounds[(int) grade];
}

public static string ToLocalizationKey(this VocalPhraseGrade grade)
{
return grade switch
{
VocalPhraseGrade.Awesome => "Awesome",
VocalPhraseGrade.Strong => "Strong",
VocalPhraseGrade.Good => "Good",
VocalPhraseGrade.Okay => "Okay",
VocalPhraseGrade.Messy => "Messy",
_ => "Awful"
};
}
}
}
11 changes: 11 additions & 0 deletions Assets/Script/Gameplay/Vocals/VocalPhraseGrade.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion Assets/Script/Menu/ScoreScreen/ScoreCards/ScoreCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ public abstract class ScoreCard<T> : MonoBehaviour, IScoreCard<T> where T : Base

public YargPlayer Player { get; private set; }

protected virtual bool ShouldShowOffsetHistogram => true;

protected RectTransform AdvancedStatsRect => _advancedStatsRect;

protected Color AdvancedAccentColor => _colorizer != null ? _colorizer.CurrentColor : Color.white;

protected TextMeshProUGUI CreateStatLabel(Transform parent, string name, TextAlignmentOptions alignment)
{
return CreateHistogramLabel(parent, name, alignment);
}

private void Awake()
{
_colorizer = GetComponent<ScoreCardColorizer>();
Expand Down Expand Up @@ -208,7 +219,14 @@ public virtual void SetCardContents()
_starPowerActivations.text = ColorizePrimary(Stats.StarPowerActivationCount);
string timeInStarPower = TimeSpan.FromSeconds(Stats.TimeInStarPower).ToString(@"m\:ss");
_timeInStarPower.text = ColorizePrimary(timeInStarPower);
BuildOffsetHistogram();
if (ShouldShowOffsetHistogram)
{
BuildOffsetHistogram();
}
else
{
SetOffsetHistogramActive(false);
}

// Set engine preset tag
var enginePresetId = Player.EnginePreset.Id;
Expand Down
Loading