Skip to content

Scoring

Bartłomiej Dach edited this page Oct 6, 2020 · 19 revisions

Total score

Score is divided into three portions: accuracy, combo, and bonus. The accuracy and combo portions together comprise the base 1000000 score, and then the bonus is added on top to determine the total score.

accuracy_portion = 0.3
combo_portion = 0.7
total_score = 1000000 * ((accuracy * accuracy_portion) + (combo / max_achievable_combo * combo_portion)) + bonus_portion

These portions can be adjusted by deriving ScoreProcessor:

public class MyScoreProcessor : ScoreProcessor
{
    // Accuracy accounts for up to 950000 of the base score.
    protected override double DefaultAccuracyPortion => 0.95;

    // Combo accounts for up to 50000 of the base score.
    protected override double DefaultComboPortion => 0.05;
}

Hit results

The HitResult class determines which portion(s) of the total score a judgement contributes to, and the amount of health gained or lost. This is defined on a per-hitobject/per-judgement level.

Score

Type Score Accuracy portion? Combo portion?¹ Bonus portion? Examples MinResult²
IgnoreMiss³ 0 Slider, SpinnerBonus, StrongHit, SwellTick, Banana -
IgnoreHit³ 0 Slider, SwellTick IgnoreMiss
Miss 0 HitCircle, Hit, Fruit, Note, TailNote -
Meh 50 HitCircle, Hit, Note, TailNote Miss
Ok 100 HitCircle, Hit, Note, TailNote Miss
Good 200 Note, TailNote Miss
Great 300 HitCircle, Hit, Fruit, Note, TailNote Miss
Perfect 350 Note, TailNote Miss
SmallTickMiss 0 SliderTail, SpinnerTick, DrumRollTick, TinyDroplet -
SmallTickHit 10 SliderTail, SpinnerTick, DrumRollTick, TinyDroplet SmallTickMiss
LargeTickMiss 0 SliderTick, Droplet -
LargeTickHit 30 SliderTick, Droplet LargeTickMiss
SmallBonus 10 StrongHit IgnoreMiss
LargeBonus 50 SpinnerBonus, Banana IgnoreMiss

¹ Contribution to the combo portion also implies the combo is increased or decreased for a hit or miss respectively.
² The minimum result is provided for reference to be used in later sections that describe hit result application.
³ All hitobjects must provide a hit result, but "Ignore" results are to be provided when the score should not be affected.

Health

The amount of health increase or decrease is defined relative to a "Perfect" hit result. By default, a "Perfect" hit result increases HP by 5%.

Type Relative addition
IgnoreMiss 0%
IgnoreHit 0%
Miss -100%
Meh -5%
Ok 50%
Good 75%
Great 100%
Perfect 105%
SmallTickMiss -50%
SmallTickHit 50%
LargeTickMiss -100%
LargeTickHit 100%
SmallBonus 50%
LargeBonus 100%

These values can be adjusted by deriving Judgement:

public class MyJudgement : Judgement
{
    protected override double HealthIncreaseFor(HitResult result)
    {
        switch (result)
        {
            case HitResult.Good:
                // Make Goods not reduce HP.
                return DEFAULT_MAX_HEALTH_INCREASE * 0.1;

            default:
                return base.HealthIncreaseFor(result);
        }
    }
}

Hitobjects

By default, all HitObject types affect the accuracy and combo portions of the score. When judged, they accept any hit result that is not a tick, bonus, or ignore result.

To change this, derive HitObject and Judgement to set the appropriate Judgement.MaxResult:

public class MySliderTick : HitObject
{
    public override Judgement CreateJudgement() => new MySliderTickJudgement();
}

public class MySliderTickJudgement : Judgement
{
    public override HitResult MaxResult => HitResult.SmallTickHit;
}

This will in-turn change the value of Judgement.MinResult as described by the table above.

Judging

To judge a DrawableHitObject, invoke DrawableHitObject.ApplyResult(Action<JudgementResult> application) and set the appropriate result within the range of Judgement.MinResult and Judgement.MaxResult for the hitobject:

MaxResult Accepted judgement result types
IgnoreHit IgnoreHit, IgnoreMiss
Meh Meh, Miss
Ok Ok, Meh, Miss
Good Good, Ok, Meh, Miss
Great Great, Good, Ok, Meh, Miss
Perfect Perfect, Great, Good, Ok, Meh, Miss
SmallTickHit SmallTickHit, SmallTickMiss
LargeTickHit LargeTickHit, LargeTickMiss
SmallBonus SmallBonus, IgnoreMiss
LargeBonus LargeBonus, IgnoreMiss
public class MyDrawableHitObject : DrawableHitObject<MyHitObject>
{
    public MyDrawableHitObject(MyHitObject hitObject)
        : base(hitObject)
    {
    }

    protected override void CheckForResult(bool userTriggered, double timeOffset)
    {
        if (!userTriggered)
        {
            if (timeOffset > HitObject.StartTime)
                ApplyResult(r => r.Type = r.Judgement.MinResult);
        }
        else
            ApplyResult(r => r.Type = r.Judgement.MaxResult);
    }

    protected override bool OnClick(ClickEvent e)
    {
        UpdateResult(true);
        return true;
    }
}

Weighting hitobjects

Nested hitobjects can be used to increase the weighting of hitobjects that are more important than others and should contribute more towards the score:

public class MyHitObject : HitObject
{
    protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
    {
        base.CreateNestedHitObjects(cancellationToken);

        // When applying a result, the same result will also be applied to the padding object (see below).
        // This results in a 2x weighting for the "MyHitObject" type.
        AddNested(new ScorePaddingObject
        {
            // Remember to set a correct start time for nested hitobjects, otherwise their judgements won't be reset correctly!
            // Judgements are only reset once time is rewound past the hitobject's start time.
            StartTime = StartTime
        });

        // For a 3x weighting, add another one!
        // AddNested(new ScorePaddingObject { StartTime = StartTime });
    }
}

public class ScorePaddingObject : HitObject
{
}

public class MyDrawableHitObject : DrawableHitObject<MyHitObject>
{
    private readonly Container<DrawableScorePaddingObject> paddingObjects;

    public MyDrawableHitObject(MyHitObject hitObject)
        : base(hitObject)
    {
        AddInternal(paddingObjects = new Container<DrawableScorePaddingObject>());
    }

    protected override void ClearNestedHitObjects()
    {
        base.ClearNestedHitObjects();
        paddingObjects.Clear();
    }

    protected override void AddNestedHitObject(DrawableHitObject hitObject)
    {
        base.AddNestedHitObject(hitObject);

        if (hitObject is DrawableScorePaddingObject pad)
            paddingObjects.Add(pad);
    }

    protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
    {
        switch (hitObject)
        {
            case ScorePaddingObject pad:
                return new DrawableScorePaddingObject(pad);

            default:
                return base.CreateNestedHitObject(hitObject);
        }
    }

    protected override void CheckForResult(bool userTriggered, double timeOffset)
    {
        if (!userTriggered)
        {
            if (timeOffset > HitObject.StartTime)
                applyResult(false);
        }
        else
            applyResult(true);
    }

    private void applyResult(bool hit)
    {
        ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
        foreach (var nested in paddingObjects)
            nested.ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
    }

    protected override bool OnClick(ClickEvent e)
    {
        UpdateResult(true);
        return true;
    }
}

public class DrawableScorePaddingObject : DrawableHitObject<ScorePaddingObject>
{
    public DrawableScorePaddingObject(ScorePaddingObject hitObject)
        : base(hitObject)
    {
    }

    public new void ApplyResult(Action<JudgementResult> application) => base.ApplyResult(application);
}