mirror of
https://github.com/ppy/osu
synced 2025-01-02 04:12:13 +00:00
Initial implementation of ScoreV2
This commit is contained in:
parent
5afe57033d
commit
3c3c812ed6
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this);
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);
|
||||
|
||||
|
@ -1,17 +1,87 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public partial class CatchScoreProcessor : ScoreProcessor
|
||||
{
|
||||
public CatchScoreProcessor()
|
||||
: base(new CatchRuleset())
|
||||
private const int combo_cap = 200;
|
||||
private const double combo_base = 4;
|
||||
|
||||
protected override double ClassicScoreMultiplier => 28;
|
||||
|
||||
private double tinyDropletScale;
|
||||
|
||||
private int maximumTinyDroplets;
|
||||
private int hitTinyDroplets;
|
||||
|
||||
public CatchScoreProcessor(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double ClassicScoreMultiplier => 28;
|
||||
protected override double ComputeTotalScore()
|
||||
{
|
||||
double fruitHitsRatio = maximumTinyDroplets == 0 ? 0 : (double)hitTinyDroplets / maximumTinyDroplets;
|
||||
|
||||
const int tiny_droplets_portion = 400000;
|
||||
|
||||
return
|
||||
(int)Math.Round
|
||||
((
|
||||
((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * ComboPortion / MaxComboPortion +
|
||||
tiny_droplets_portion * tinyDropletScale * fruitHitsRatio +
|
||||
BonusPortion
|
||||
) * ScoreMultiplier);
|
||||
}
|
||||
|
||||
protected override void AddScoreChange(JudgementResult result)
|
||||
{
|
||||
var change = computeScoreChange(result);
|
||||
ComboPortion += change.combo;
|
||||
BonusPortion += change.bonus;
|
||||
hitTinyDroplets += change.tinyDropletHits;
|
||||
}
|
||||
|
||||
protected override void RemoveScoreChange(JudgementResult result)
|
||||
{
|
||||
var change = computeScoreChange(result);
|
||||
ComboPortion -= change.combo;
|
||||
BonusPortion -= change.bonus;
|
||||
hitTinyDroplets -= change.tinyDropletHits;
|
||||
}
|
||||
|
||||
private (double combo, double bonus, int tinyDropletHits) computeScoreChange(JudgementResult result)
|
||||
{
|
||||
if (result.HitObject is TinyDroplet)
|
||||
return (0, 0, 1);
|
||||
|
||||
if (result.Type.IsBonus())
|
||||
return (0, Judgement.ToNumericResult(result.Type), 0);
|
||||
|
||||
return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(combo_cap, combo_base)), 0, 0);
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
if (storeResults)
|
||||
{
|
||||
maximumTinyDroplets = hitTinyDroplets;
|
||||
|
||||
if (maximumTinyDroplets + MaxBasicJudgements == 0)
|
||||
tinyDropletScale = 0;
|
||||
else
|
||||
tinyDropletScale = (double)maximumTinyDroplets / (maximumTinyDroplets + MaxBasicJudgements);
|
||||
}
|
||||
|
||||
hitTinyDroplets = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
|
||||
|
||||
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime);
|
||||
|
||||
|
@ -1,23 +1,54 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
internal partial class ManiaScoreProcessor : ScoreProcessor
|
||||
public partial class ManiaScoreProcessor : ScoreProcessor
|
||||
{
|
||||
public ManiaScoreProcessor()
|
||||
: base(new ManiaRuleset())
|
||||
private const double combo_base = 4;
|
||||
|
||||
protected override double ClassicScoreMultiplier => 16;
|
||||
|
||||
public ManiaScoreProcessor(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double DefaultAccuracyPortion => 0.99;
|
||||
protected override double ComputeTotalScore()
|
||||
{
|
||||
return
|
||||
(int)Math.Round
|
||||
((
|
||||
200000 * ComboPortion / MaxComboPortion +
|
||||
800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
|
||||
BonusPortion
|
||||
) * ScoreMultiplier);
|
||||
}
|
||||
|
||||
protected override double DefaultComboPortion => 0.01;
|
||||
protected override void AddScoreChange(JudgementResult result)
|
||||
{
|
||||
var change = computeScoreChange(result);
|
||||
ComboPortion += change.combo;
|
||||
BonusPortion += change.bonus;
|
||||
}
|
||||
|
||||
protected override double ClassicScoreMultiplier => 16;
|
||||
protected override void RemoveScoreChange(JudgementResult result)
|
||||
{
|
||||
var change = computeScoreChange(result);
|
||||
ComboPortion -= change.combo;
|
||||
BonusPortion -= change.bonus;
|
||||
}
|
||||
|
||||
private (double combo, double bonus) computeScoreChange(JudgementResult result)
|
||||
{
|
||||
if (result.Type.IsBonus())
|
||||
return (0, Judgement.ToNumericResult(result.Type));
|
||||
|
||||
return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
|
||||
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this);
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this);
|
||||
|
||||
|
@ -1,38 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using System;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public partial class OsuScoreProcessor : ScoreProcessor
|
||||
{
|
||||
public OsuScoreProcessor()
|
||||
: base(new OsuRuleset())
|
||||
protected override double ClassicScoreMultiplier => 36;
|
||||
|
||||
public OsuScoreProcessor(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double ClassicScoreMultiplier => 36;
|
||||
|
||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||
|
||||
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
|
||||
protected override double ComputeTotalScore()
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case HitCircle:
|
||||
return new OsuHitCircleJudgementResult(hitObject, judgement);
|
||||
|
||||
default:
|
||||
return new OsuJudgementResult(hitObject, judgement);
|
||||
}
|
||||
return
|
||||
(int)Math.Round
|
||||
((
|
||||
700000 * ComboPortion / MaxComboPortion +
|
||||
300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
|
||||
BonusPortion
|
||||
) * ScoreMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
internal partial class TaikoScoreProcessor : ScoreProcessor
|
||||
public partial class TaikoScoreProcessor : ScoreProcessor
|
||||
{
|
||||
public TaikoScoreProcessor()
|
||||
: base(new TaikoRuleset())
|
||||
private const double combo_base = 4;
|
||||
|
||||
protected override double ClassicScoreMultiplier => 22;
|
||||
|
||||
public TaikoScoreProcessor(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double DefaultAccuracyPortion => 0.75;
|
||||
protected override double ComputeTotalScore()
|
||||
{
|
||||
return
|
||||
(int)Math.Round
|
||||
((
|
||||
250000 * ComboPortion / MaxComboPortion +
|
||||
750000 * Math.Pow(Accuracy.Value, 3.6) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
|
||||
BonusPortion
|
||||
) * ScoreMultiplier);
|
||||
}
|
||||
|
||||
protected override double DefaultComboPortion => 0.25;
|
||||
protected override void AddScoreChange(JudgementResult result)
|
||||
{
|
||||
var change = computeScoreChange(result);
|
||||
BonusPortion += change.bonus;
|
||||
ComboPortion += change.combo;
|
||||
}
|
||||
|
||||
protected override double ClassicScoreMultiplier => 22;
|
||||
protected override void RemoveScoreChange(JudgementResult result)
|
||||
{
|
||||
var change = computeScoreChange(result);
|
||||
BonusPortion -= change.bonus;
|
||||
ComboPortion -= change.combo;
|
||||
}
|
||||
|
||||
private (double combo, double bonus) computeScoreChange(JudgementResult result)
|
||||
{
|
||||
double hitValue = Judgement.ToNumericResult(result.Type);
|
||||
|
||||
if (result.HitObject is StrongNestedHitObject strong)
|
||||
{
|
||||
double strongBonus = strong.Parent is DrumRollTick ? 3 : 7;
|
||||
hitValue *= strongBonus;
|
||||
}
|
||||
|
||||
if (result.Type.IsBonus())
|
||||
return (0, hitValue);
|
||||
|
||||
return (hitValue * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
|
||||
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
|
||||
|
||||
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TaikoHealthProcessor();
|
||||
|
||||
|
@ -157,7 +157,8 @@ namespace osu.Game.Online.Spectator
|
||||
Accuracy.Value = frame.Header.Accuracy;
|
||||
Combo.Value = frame.Header.Combo;
|
||||
|
||||
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
|
||||
// Todo:
|
||||
// TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -66,7 +66,9 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
// calculate total score
|
||||
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.Mods.Value = perfectPlay.Mods;
|
||||
perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
|
||||
|
||||
// Todo:
|
||||
// perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
|
||||
|
||||
// compute rank achieved
|
||||
// default to SS, then adjust the rank with mods
|
||||
|
@ -65,7 +65,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
scoreProcessor.PopulateScore(score);
|
||||
score.Statistics[result.Type]++;
|
||||
|
||||
return scoreProcessor.ComputeAccuracy(score);
|
||||
// Todo:
|
||||
return 0;
|
||||
// return scoreProcessor.ComputeAccuracy(score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ namespace osu.Game.Rulesets
|
||||
/// Creates a <see cref="ScoreProcessor"/> for this <see cref="Ruleset"/>.
|
||||
/// </summary>
|
||||
/// <returns>The score processor.</returns>
|
||||
public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
|
||||
public virtual ScoreProcessor CreateScoreProcessor() => new DefaultScoreProcessor(this);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthProcessor"/> for this <see cref="Ruleset"/>.
|
||||
@ -381,4 +381,23 @@ namespace osu.Game.Rulesets
|
||||
/// </summary>
|
||||
public virtual RulesetSetupSection? CreateEditorSetupSection() => null;
|
||||
}
|
||||
|
||||
public partial class DefaultScoreProcessor : ScoreProcessor
|
||||
{
|
||||
public DefaultScoreProcessor(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double ComputeTotalScore()
|
||||
{
|
||||
return
|
||||
(int)Math.Round
|
||||
((
|
||||
700000 * ComboPortion / MaxComboPortion +
|
||||
300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
|
||||
BonusPortion
|
||||
) * ScoreMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Localisation;
|
||||
@ -20,8 +18,10 @@ using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
public partial class ScoreProcessor : JudgementProcessor
|
||||
public abstract partial class ScoreProcessor : JudgementProcessor
|
||||
{
|
||||
protected const double MAX_SCORE = 1000000;
|
||||
|
||||
private const double accuracy_cutoff_x = 1;
|
||||
private const double accuracy_cutoff_s = 0.95;
|
||||
private const double accuracy_cutoff_a = 0.9;
|
||||
@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private const double accuracy_cutoff_c = 0.7;
|
||||
private const double accuracy_cutoff_d = 0;
|
||||
|
||||
private const double max_score = 1000000;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
|
||||
/// </summary>
|
||||
@ -89,16 +87,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public IReadOnlyList<HitEvent> HitEvents => hitEvents;
|
||||
|
||||
/// <summary>
|
||||
/// The default portion of <see cref="max_score"/> awarded for hitting <see cref="HitObject"/>s accurately. Defaults to 30%.
|
||||
/// </summary>
|
||||
protected virtual double DefaultAccuracyPortion => 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// The default portion of <see cref="max_score"/> awarded for achieving a high combo. Default to 70%.
|
||||
/// </summary>
|
||||
protected virtual double DefaultComboPortion => 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// An arbitrary multiplier to scale scores in the <see cref="ScoringMode.Classic"/> scoring mode.
|
||||
/// </summary>
|
||||
@ -109,8 +97,45 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public readonly Ruleset Ruleset;
|
||||
|
||||
private readonly double accuracyPortion;
|
||||
private readonly double comboPortion;
|
||||
/// <summary>
|
||||
/// The sum of all basic judgements at the current time.
|
||||
/// </summary>
|
||||
private double currentBasicScore;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum sum of basic judgements at the current time.
|
||||
/// </summary>
|
||||
private double currentMaxBasicScore;
|
||||
|
||||
/// <summary>
|
||||
/// The total count of basic judgements in the beatmap.
|
||||
/// </summary>
|
||||
protected int MaxBasicJudgements { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current count of basic judgements by the player.
|
||||
/// </summary>
|
||||
protected int CurrentBasicJudgements { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current combo score.
|
||||
/// </summary>
|
||||
protected double ComboPortion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum achievable combo score.
|
||||
/// </summary>
|
||||
protected double MaxComboPortion { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current bonus score.
|
||||
/// </summary>
|
||||
protected double BonusPortion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total score multiplier.
|
||||
/// </summary>
|
||||
protected double ScoreMultiplier { get; private set; } = 1;
|
||||
|
||||
public Dictionary<HitResult, int> MaximumStatistics
|
||||
{
|
||||
@ -123,27 +148,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
private ScoringValues maximumScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// Scoring values for the current play assuming all perfect hits.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
|
||||
/// </remarks>
|
||||
private ScoringValues currentMaximumScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// Scoring values for the current play.
|
||||
/// </summary>
|
||||
private ScoringValues currentScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
|
||||
/// Only populated via <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoreInfo)"/> or <see cref="ResetFromReplayFrame"/>.
|
||||
/// </summary>
|
||||
private HitResult? maxBasicResult;
|
||||
|
||||
private bool beatmapApplied;
|
||||
|
||||
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
|
||||
@ -152,18 +156,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
|
||||
private HitObject? lastHitObject;
|
||||
|
||||
private double scoreMultiplier = 1;
|
||||
|
||||
public ScoreProcessor(Ruleset ruleset)
|
||||
protected ScoreProcessor(Ruleset ruleset)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
|
||||
accuracyPortion = DefaultAccuracyPortion;
|
||||
comboPortion = DefaultComboPortion;
|
||||
|
||||
if (!Precision.AlmostEquals(1.0, accuracyPortion + comboPortion))
|
||||
throw new InvalidOperationException($"{nameof(DefaultAccuracyPortion)} + {nameof(DefaultComboPortion)} must equal 1.");
|
||||
|
||||
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
|
||||
Accuracy.ValueChanged += accuracy =>
|
||||
{
|
||||
@ -175,10 +171,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
Mode.ValueChanged += _ => updateScore();
|
||||
Mods.ValueChanged += mods =>
|
||||
{
|
||||
scoreMultiplier = 1;
|
||||
ScoreMultiplier = 1;
|
||||
|
||||
foreach (var m in mods.NewValue)
|
||||
scoreMultiplier *= m.ScoreMultiplier;
|
||||
ScoreMultiplier *= m.ScoreMultiplier;
|
||||
|
||||
updateScore();
|
||||
};
|
||||
@ -200,10 +196,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
|
||||
|
||||
// Always update the maximum scoring values.
|
||||
applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
|
||||
currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
|
||||
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
@ -212,8 +204,13 @@ namespace osu.Game.Rulesets.Scoring
|
||||
else if (result.Type.BreaksCombo())
|
||||
Combo.Value = 0;
|
||||
|
||||
applyResult(result.Type, ref currentScoringValues);
|
||||
currentScoringValues.MaxCombo = HighestCombo.Value;
|
||||
if (result.Type.IsBasic())
|
||||
CurrentBasicJudgements++;
|
||||
|
||||
currentMaxBasicScore += Judgement.ToNumericResult(result.Judgement.MaxResult);
|
||||
currentBasicScore += Judgement.ToNumericResult(result.Type);
|
||||
|
||||
AddScoreChange(result);
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
@ -221,20 +218,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
updateScore();
|
||||
}
|
||||
|
||||
private static void applyResult(HitResult result, ref ScoringValues scoringValues)
|
||||
{
|
||||
if (!result.IsScorable())
|
||||
return;
|
||||
|
||||
if (result.IsBonus())
|
||||
scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||
else
|
||||
scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||
|
||||
if (result.IsBasic())
|
||||
scoringValues.CountBasicHitObjects++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
@ -253,15 +236,16 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
|
||||
|
||||
// Always update the maximum scoring values.
|
||||
revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
|
||||
currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
|
||||
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
revertResult(result.Type, ref currentScoringValues);
|
||||
currentScoringValues.MaxCombo = HighestCombo.Value;
|
||||
if (result.Type.IsBasic())
|
||||
CurrentBasicJudgements--;
|
||||
|
||||
currentMaxBasicScore -= Judgement.ToNumericResult(result.Judgement.MaxResult);
|
||||
currentBasicScore -= Judgement.ToNumericResult(result.Type);
|
||||
|
||||
RemoveScoreChange(result);
|
||||
|
||||
Debug.Assert(hitEvents.Count > 0);
|
||||
lastHitObject = hitEvents[^1].LastHitObject;
|
||||
@ -270,111 +254,31 @@ namespace osu.Game.Rulesets.Scoring
|
||||
updateScore();
|
||||
}
|
||||
|
||||
private static void revertResult(HitResult result, ref ScoringValues scoringValues)
|
||||
protected virtual void AddScoreChange(JudgementResult result)
|
||||
{
|
||||
if (!result.IsScorable())
|
||||
return;
|
||||
|
||||
if (result.IsBonus())
|
||||
scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||
if (result.Type.IsBonus())
|
||||
BonusPortion += Judgement.ToNumericResult(result.Type);
|
||||
else
|
||||
scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||
ComboPortion += Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d);
|
||||
}
|
||||
|
||||
if (result.IsBasic())
|
||||
scoringValues.CountBasicHitObjects--;
|
||||
protected virtual void RemoveScoreChange(JudgementResult result)
|
||||
{
|
||||
if (result.Type.IsBonus())
|
||||
BonusPortion -= Judgement.ToNumericResult(result.Type);
|
||||
else
|
||||
ComboPortion -= Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d);
|
||||
}
|
||||
|
||||
private void updateScore()
|
||||
{
|
||||
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
|
||||
MinimumAccuracy.Value = maximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 0;
|
||||
MaximumAccuracy.Value = maximumScoringValues.BaseScore > 0
|
||||
? (double)(currentScoringValues.BaseScore + (maximumScoringValues.BaseScore - currentMaximumScoringValues.BaseScore)) / maximumScoringValues.BaseScore
|
||||
: 1;
|
||||
TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues);
|
||||
Accuracy.Value = currentMaxBasicScore > 0 ? currentBasicScore / currentMaxBasicScore : 1;
|
||||
|
||||
// Todo: Classic/Standardised
|
||||
TotalScore.Value = (long)Math.Round(ComputeTotalScore());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the accuracy of a given <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The score's accuracy.</returns>
|
||||
[Pure]
|
||||
public double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
|
||||
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
|
||||
|
||||
return maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score of a given <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
|
||||
/// </remarks>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
[Pure]
|
||||
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
extractScoringValues(scoreInfo, out var current, out var maximum);
|
||||
|
||||
return computeScore(mode, current, maximum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score from scoring values.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="current">The current scoring values.</param>
|
||||
/// <param name="maximum">The maximum scoring values.</param>
|
||||
/// <returns>The total score computed from the given scoring values.</returns>
|
||||
[Pure]
|
||||
private long computeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
|
||||
{
|
||||
double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
|
||||
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score from individual scoring components.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="accuracyRatio">The accuracy percentage achieved by the player.</param>
|
||||
/// <param name="comboRatio">The portion of the max combo achieved by the player.</param>
|
||||
/// <param name="bonusScore">The total bonus score.</param>
|
||||
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
|
||||
/// <returns>The total score computed from the given scoring component ratios.</returns>
|
||||
[Pure]
|
||||
public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects)
|
||||
{
|
||||
double accuracyScore = accuracyPortion * accuracyRatio;
|
||||
double comboScore = comboPortion * comboRatio;
|
||||
double rawScore = (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
default:
|
||||
case ScoringMode.Standardised:
|
||||
return (long)Math.Round(rawScore);
|
||||
|
||||
case ScoringMode.Classic:
|
||||
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
|
||||
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
|
||||
double scaledRawScore = rawScore / max_score;
|
||||
return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier);
|
||||
}
|
||||
}
|
||||
protected abstract double ComputeTotalScore();
|
||||
|
||||
/// <summary>
|
||||
/// Resets this ScoreProcessor to a default state.
|
||||
@ -389,7 +293,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
if (storeResults)
|
||||
{
|
||||
maximumScoringValues = currentScoringValues;
|
||||
MaxComboPortion = ComboPortion;
|
||||
MaxBasicJudgements = CurrentBasicJudgements;
|
||||
|
||||
maximumResultCounts.Clear();
|
||||
maximumResultCounts.AddRange(scoreResultCounts);
|
||||
@ -397,8 +302,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
|
||||
currentScoringValues = default;
|
||||
currentMaximumScoringValues = default;
|
||||
currentBasicScore = 0;
|
||||
currentMaxBasicScore = 0;
|
||||
CurrentBasicJudgements = 0;
|
||||
ComboPortion = 0;
|
||||
BonusPortion = 0;
|
||||
|
||||
TotalScore.Value = 0;
|
||||
Accuracy.Value = 1;
|
||||
@ -406,6 +314,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
Rank.Disabled = false;
|
||||
Rank.Value = ScoreRank.X;
|
||||
HighestCombo.Value = 0;
|
||||
|
||||
currentBasicScore = 0;
|
||||
currentMaxBasicScore = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -428,7 +339,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
|
||||
|
||||
// Populate total score after everything else.
|
||||
score.TotalScore = ComputeScore(ScoringMode.Standardised, score);
|
||||
score.TotalScore = TotalScore.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -452,12 +363,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (frame.Header == null)
|
||||
return;
|
||||
|
||||
extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
|
||||
currentScoringValues.BaseScore = current.BaseScore;
|
||||
currentScoringValues.MaxCombo = frame.Header.MaxCombo;
|
||||
currentMaximumScoringValues.BaseScore = maximum.BaseScore;
|
||||
currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
|
||||
|
||||
Combo.Value = frame.Header.Combo;
|
||||
HighestCombo.Value = frame.Header.MaxCombo;
|
||||
|
||||
@ -469,105 +374,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
OnResetFromReplayFrame?.Invoke();
|
||||
}
|
||||
|
||||
#region ScoringValue extraction
|
||||
|
||||
/// <summary>
|
||||
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
|
||||
/// <list type="bullet">
|
||||
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
|
||||
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
|
||||
/// </list>
|
||||
/// Consumers are expected to more accurately fill in the above values through external means.
|
||||
/// <para>
|
||||
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
|
||||
/// <see cref="computeScore(osu.Game.Rulesets.Scoring.ScoringMode,ScoringValues,ScoringValues)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="scoreInfo">The score to extract scoring values from.</param>
|
||||
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
|
||||
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
|
||||
[Pure]
|
||||
private void extractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
|
||||
{
|
||||
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
|
||||
current.MaxCombo = scoreInfo.MaxCombo;
|
||||
|
||||
if (scoreInfo.MaximumStatistics.Count > 0)
|
||||
extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
|
||||
/// <list type="bullet">
|
||||
/// <item>The current <see cref="ScoringValues.MaxCombo"/> will always be 0.</item>
|
||||
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
|
||||
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
|
||||
/// </list>
|
||||
/// Consumers are expected to more accurately fill in the above values (especially the current <see cref="ScoringValues.MaxCombo"/>) via external means (e.g. <see cref="ScoreInfo"/>).
|
||||
/// </remarks>
|
||||
/// <param name="statistics">The hit statistics to extract scoring values from.</param>
|
||||
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
|
||||
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
|
||||
[Pure]
|
||||
private void extractScoringValues(IReadOnlyDictionary<HitResult, int> statistics, out ScoringValues current, out ScoringValues maximum)
|
||||
{
|
||||
current = default;
|
||||
maximum = default;
|
||||
|
||||
foreach ((HitResult result, int count) in statistics)
|
||||
{
|
||||
if (!result.IsScorable())
|
||||
continue;
|
||||
|
||||
if (result.IsBonus())
|
||||
current.BonusScore += count * Judgement.ToNumericResult(result);
|
||||
|
||||
if (result.AffectsAccuracy())
|
||||
{
|
||||
// The maximum result of this judgement if it wasn't a miss.
|
||||
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
|
||||
HitResult maxResult;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.LargeTickMiss:
|
||||
maxResult = HitResult.LargeTickHit;
|
||||
break;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.SmallTickMiss:
|
||||
maxResult = HitResult.SmallTickHit;
|
||||
break;
|
||||
|
||||
default:
|
||||
maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
|
||||
break;
|
||||
}
|
||||
|
||||
current.BaseScore += count * Judgement.ToNumericResult(result);
|
||||
maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
|
||||
}
|
||||
|
||||
if (result.AffectsCombo())
|
||||
maximum.MaxCombo += count;
|
||||
|
||||
if (result.IsBasic())
|
||||
{
|
||||
current.CountBasicHitObjects += count;
|
||||
maximum.CountBasicHitObjects += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
@ -629,32 +435,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
|
||||
/// </summary>
|
||||
private struct ScoringValues
|
||||
{
|
||||
/// <summary>
|
||||
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||
/// </summary>
|
||||
public long BaseScore;
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||
/// </summary>
|
||||
public long BonusScore;
|
||||
|
||||
/// <summary>
|
||||
/// The highest achieved combo.
|
||||
/// </summary>
|
||||
public int MaxCombo;
|
||||
|
||||
/// <summary>
|
||||
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
|
||||
/// </summary>
|
||||
public int CountBasicHitObjects;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ScoringMode
|
||||
|
@ -115,7 +115,9 @@ namespace osu.Game.Scoring
|
||||
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.Mods.Value = score.Mods;
|
||||
|
||||
return scoreProcessor.ComputeScore(mode, score);
|
||||
// Todo:
|
||||
return 0;
|
||||
// return scoreProcessor.ComputeScore(mode, score);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -67,7 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
|
||||
|
||||
Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo);
|
||||
// Todo:
|
||||
// Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
Loading…
Reference in New Issue
Block a user