Table of Contents
Total score
Score is divided into three portions: accuracy, combo, and bonus. The calculation follows a similar algorithm to Score V2 (from stable). Please check the ScoreProcessor implementation for specifics (ApplyResultInternal should be a good place to start).
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² |
|---|---|---|---|---|---|---|
| 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 | 300⁴ | ✅ | ✅ | ❌ | Note, TailNote | Miss |
| SmallTickMiss | 0 | ✅ | ❌ | ❌ | SpinnerTick, DrumRollTick, TinyDroplet | - |
| SmallTickHit | 10 | ✅ | ❌ | ❌ | SpinnerTick, DrumRollTick, TinyDroplet | SmallTickMiss |
| LargeTickMiss | 0 | ✅ | ✅ | ❌ | SliderTick, Droplet | - |
| LargeTickHit | 30 | ✅ | ✅ | ❌ | SliderTick, Droplet, SliderTail⁵ | LargeTickMiss |
| SmallBonus | 10 | ❌ | ❌ | ✅ | StrongHit, Note, TailNote | IgnoreMiss |
| LargeBonus | 50 | ❌ | ❌ | ✅ | SpinnerBonus, Banana | IgnoreMiss |
| IgnoreMiss³ | 0 | ❌ | ❌ | ❌ | Slider, SliderTail, SpinnerBonus, StrongHit, SwellTick, Banana | - |
| IgnoreHit³ | 0 | ❌ | ❌ | ❌ | Slider, SwellTick | IgnoreMiss |
| ComboBreak | 0 | ❌ | ✅ | ❌ | HoldNoteBody | - |
| SliderTailHit | 150 | ✅ | ✅ | ❌ | SliderTail | IgnoreMiss |
| LegacyComboIncrease | 0 | ❌ | ✅ | ❌ | Not usable (legacy only) | - |
¹ Contribution to the combo portion also implies the combo is increased for a hit, or reset on a miss.
² 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.
⁴ Objects using the perfect judgement which should provide additional score / accuracy above Great should do so via the use of nested objects with tick / bonus judgements.
⁵ In this particular case, the MinResult is an IgnoreMiss, which means that missing the slider end does not break combo.
Health
The amount of health increase or decrease is defined relative to a "Great" hit result. By default, a "Great" hit result increases HP by 5%.
| Type | Relative addition |
|---|---|
| IgnoreMiss | 0% |
| IgnoreHit | 0% |
| Miss | -200% |
| 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, ComboBreak |
| 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);
}