// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using System.Diagnostics; using osu.Framework.Configuration; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Scoring { public abstract class ScoreProcessor { /// /// Invoked when the is in a failed state. /// This may occur regardless of whether an event is invoked. /// Return true if the fail was permitted. /// public event Func Failed; /// /// Invoked when all s have been judged. /// public event Action AllJudged; /// /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by the . /// public event Action NewJudgement; /// /// Additional conditions on top of that cause a failing state. /// public event Func FailConditions; /// /// The current total score. /// public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 }; /// /// The current accuracy. /// public readonly BindableDouble Accuracy = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; /// /// The current health. /// public readonly BindableDouble Health = new BindableDouble { MinValue = 0, MaxValue = 1 }; /// /// The current combo. /// public readonly BindableInt Combo = new BindableInt(); /// /// The current rank. /// public readonly Bindable Rank = new Bindable(ScoreRank.X); /// /// THe highest combo achieved by this score. /// public readonly BindableInt HighestCombo = new BindableInt(); /// /// Whether all s have been processed. /// protected virtual bool HasCompleted => false; /// /// Whether this ScoreProcessor has already triggered the failed state. /// public virtual bool HasFailed { get; private set; } /// /// The default conditions for failing. /// protected virtual bool DefaultFailCondition => Health.Value == Health.MinValue; protected ScoreProcessor() { Combo.ValueChanged += delegate { HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); }; Accuracy.ValueChanged += delegate { Rank.Value = rankFrom(Accuracy.Value); }; } private ScoreRank rankFrom(double acc) { if (acc == 1) return ScoreRank.X; if (acc > 0.95) return ScoreRank.S; if (acc > 0.9) return ScoreRank.A; if (acc > 0.8) return ScoreRank.B; if (acc > 0.7) return ScoreRank.C; return ScoreRank.D; } /// /// Resets this ScoreProcessor to a default state. /// /// Whether to store the current state of the for future use. protected virtual void Reset(bool storeResults) { TotalScore.Value = 0; Accuracy.Value = 1; Health.Value = 1; Combo.Value = 0; Rank.Value = ScoreRank.X; HighestCombo.Value = 0; HasFailed = false; } /// /// Checks if the score is in a failed state and notifies subscribers. /// /// This can only ever notify subscribers once. /// /// protected void UpdateFailed() { if (HasFailed) return; if (!DefaultFailCondition && FailConditions?.Invoke(this) != true) return; if (Failed?.Invoke() != false) HasFailed = true; } /// /// Notifies subscribers of that a new judgement has occurred. /// /// The judgement to notify subscribers of. protected void NotifyNewJudgement(Judgement judgement) { NewJudgement?.Invoke(judgement); if (HasCompleted) AllJudged?.Invoke(); } /// /// Retrieve a score populated with data for the current play this processor is responsible for. /// public virtual void PopulateScore(Score score) { score.TotalScore = TotalScore; score.Combo = Combo; score.MaxCombo = HighestCombo; score.Accuracy = Accuracy; score.Rank = Rank; score.Date = DateTimeOffset.Now; score.Health = Health; } } public class ScoreProcessor : ScoreProcessor where TObject : HitObject { private const double base_portion = 0.3; private const double combo_portion = 0.7; private const double max_score = 1000000; public readonly Bindable Mode = new Bindable(); protected sealed override bool HasCompleted => Hits == MaxHits; protected int MaxHits { get; private set; } protected int Hits { get; private set; } private double maxHighestCombo; private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; private double bonusScore; protected ScoreProcessor() { } public ScoreProcessor(RulesetContainer rulesetContainer) { Debug.Assert(base_portion + combo_portion == 1.0); rulesetContainer.OnJudgement += AddJudgement; rulesetContainer.OnJudgementRemoved += RemoveJudgement; SimulateAutoplay(rulesetContainer.Beatmap); Reset(true); if (maxBaseScore == 0 || maxHighestCombo == 0) { Mode.Value = ScoringMode.Exponential; Mode.Disabled = true; } } /// /// Simulates an autoplay of s that will be judged by this /// by adding s for each in the . /// /// This is required for to work, otherwise will be used. /// /// /// The containing the s that will be judged by this . protected virtual void SimulateAutoplay(Beatmap beatmap) { } /// /// Adds a judgement to this ScoreProcessor. /// /// The judgement to add. protected void AddJudgement(Judgement judgement) { OnNewJudgement(judgement); updateScore(); UpdateFailed(); NotifyNewJudgement(judgement); } protected void RemoveJudgement(Judgement judgement) { OnJudgementRemoved(judgement); updateScore(); } /// /// Applies a judgement. /// /// The judgement to apply/ protected virtual void OnNewJudgement(Judgement judgement) { judgement.ComboAtJudgement = Combo; judgement.HighestComboAtJudgement = HighestCombo; if (judgement.AffectsCombo) { switch (judgement.Result) { case HitResult.None: break; case HitResult.Miss: Combo.Value = 0; break; default: Combo.Value++; break; } baseScore += judgement.NumericResult; rollingMaxBaseScore += judgement.MaxNumericResult; Hits++; } else if (judgement.IsHit) bonusScore += judgement.NumericResult; } /// /// Removes a judgement. This should reverse everything in . /// /// The judgement to remove. protected virtual void OnJudgementRemoved(Judgement judgement) { Combo.Value = judgement.ComboAtJudgement; HighestCombo.Value = judgement.HighestComboAtJudgement; if (judgement.AffectsCombo) { baseScore -= judgement.NumericResult; rollingMaxBaseScore -= judgement.MaxNumericResult; Hits--; } else if (judgement.IsHit) bonusScore -= judgement.NumericResult; } private void updateScore() { if (rollingMaxBaseScore != 0) Accuracy.Value = baseScore / rollingMaxBaseScore; switch (Mode.Value) { case ScoringMode.Standardised: TotalScore.Value = max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo / maxHighestCombo) + bonusScore; break; case ScoringMode.Exponential: TotalScore.Value = (baseScore + bonusScore) * Math.Log(HighestCombo + 1, 2); break; } } protected override void Reset(bool storeResults) { if (storeResults) { MaxHits = Hits; maxHighestCombo = HighestCombo; maxBaseScore = baseScore; } base.Reset(storeResults); Hits = 0; baseScore = 0; rollingMaxBaseScore = 0; bonusScore = 0; } } public enum ScoringMode { Standardised, Exponential } }