// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using osu.Framework.Configuration; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Objects.Drawables; 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; /// /// 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 highest combo achieved by this score. /// public readonly BindableInt HighestCombo = new BindableInt(); /// /// Whether all s have been processed. /// protected virtual bool HasCompleted => false; /// /// Whether the score is in a failed state. /// public virtual bool HasFailed => Health.Value == Health.MinValue; /// /// Whether this ScoreProcessor has already triggered the failed state. /// private bool alreadyFailed; protected ScoreProcessor() { Combo.ValueChanged += delegate { HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); }; Reset(); } 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. /// protected virtual void Reset() { TotalScore.Value = 0; Accuracy.Value = 1; Health.Value = 1; Combo.Value = 0; HighestCombo.Value = 0; alreadyFailed = false; } /// /// Checks if the score is in a failed state and notifies subscribers. /// /// This can only ever notify subscribers once. /// /// protected void UpdateFailed() { if (alreadyFailed || !HasFailed) return; if (Failed?.Invoke() != false) alreadyFailed = 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 = rankFrom(Accuracy); score.Date = DateTimeOffset.Now; score.Health = Health; } } public abstract class ScoreProcessor : ScoreProcessor where TObject : HitObject { private const double max_score = 1000000; public readonly Bindable Mode = new Bindable(ScoringMode.Exponential); protected sealed override bool HasCompleted => Hits == MaxHits; protected virtual double ComboPortion => 0.5f; protected virtual double AccuracyPortion => 0.5f; protected readonly int MaxHits; protected int Hits { get; private set; } private readonly double maxComboScore; private double comboScore; protected ScoreProcessor() { } protected ScoreProcessor(RulesetContainer rulesetContainer) { rulesetContainer.OnJudgement += AddJudgement; SimulateAutoplay(rulesetContainer.Beatmap); maxComboScore = comboScore; MaxHits = Hits; Reset(); } /// /// Simulates an autoplay of s that will be judged by this /// by adding s for each in the . /// /// 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) { if (judgement.AffectsCombo) { switch (judgement.Result) { case HitResult.Miss: Combo.Value = 0; break; default: Combo.Value++; break; } } OnNewJudgement(judgement); NotifyNewJudgement(judgement); UpdateFailed(); } protected virtual void OnNewJudgement(Judgement judgement) { double bonusScore = 0; if (judgement.AffectsCombo) { switch (judgement.Result) { case HitResult.None: break; case HitResult.Miss: Combo.Value = 0; break; default: Combo.Value++; break; } comboScore += judgement.NumericResult; } else if (judgement.IsHit) bonusScore += judgement.NumericResult; if (judgement.AffectsAccuracy && judgement.IsHit) Hits++; switch (Mode.Value) { case ScoringMode.Standardised: TotalScore.Value = max_score * (ComboPortion * comboScore / maxComboScore + AccuracyPortion * Hits / MaxHits) + bonusScore; break; case ScoringMode.Exponential: TotalScore.Value = (comboScore + bonusScore) * Math.Log(HighestCombo + 1, 2); break; } } protected override void Reset() { base.Reset(); Hits = 0; comboScore = 0; } } public enum ScoringMode { Standardised, Exponential } }