-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Scoring
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;
}
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.
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.
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);
}
}
}
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.
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;
}
}
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);
}