1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:33:22 +08:00

Created Scoring (markdown)

Dan Balasescu 2020-10-01 15:40:29 +09:00
parent 0539d9c002
commit cbfb489b40

248
Scoring.md Normal file

@ -0,0 +1,248 @@
# 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`:
```csharp
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, SwellTick, StrongHit, Banana, SpinnerBonus | - |
| IgnoreHit³ | 0 | ❌ | ❌ | ❌ | Slider, SwellTick | IgnoreMiss |
| Miss | 0 | ✅ | ✅ | ❌ | HitCircle | - |
| Meh | 50 | ✅ | ✅ | ❌ | HitCircle | Miss |
| Ok | 100 | ✅ | ✅ | ❌ | HitCircle | Miss |
| Good | 200 | ✅ | ✅ | ❌ | HitCircle | Miss |
| Great | 300 | ✅ | ✅ | ❌ | HitCircle | Miss |
| Perfect | 350 | ✅ | ✅ | ❌ | HitCircle | Miss |
| SmallTickMiss | 0 | ✅ | ❌ | ❌ | TinyDroplet, SpinnerTick | - |
| SmallTickHit | 10 | ✅ | ❌ | ❌ | TinyDroplet, SpinnerTick | SmallTickMiss |
| LargeTickMiss | 0 | ✅ | ✅ | ❌ | Droplet, SliderTick | - |
| LargeTickHit | 30 | ✅ | ✅ | ❌ | Droplet, SliderTick | LargeTickMiss |
| SmallBonus | 10 | ❌ | ❌ | ✅ | StrongHit | IgnoreMiss |
| LargeBonus | 50 | ❌ | ❌ | ✅ | Banana, SpinnerBonus | 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 "Pefect" hit result. By default, a "Perfect" hit result increases HP by 5%.
| Type | Relative addition |
| ------------- | :---------------: |
| IgnoreMiss | 0% |
| IgnoreHit | 0% |
| Miss | -100% |
| Meh | -50% |
| Ok | -30% |
| Good | -10% |
| Great | 80% |
| Perfect | 100% |
| SmallTickMiss | -5% |
| SmallTickHit | 5% |
| LargeTickMiss | -10% |
| LargeTickHit | 10% |
| SmallBonus | 10% |
| LargeBonus | 20% |
These values can be adjusted by deriving `Judgement`:
```csharp
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`:
```csharp
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 |
```csharp
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:
```csharp
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());
// For a 3x weighting, add another one!
// AddNested(new ScorePaddingObject());
}
}
public class ScorePaddingObject : HitObject
{
}
public class MyDrawableHitObject : DrawableHitObject<MyHitObject>
{
private readonly List<DrawableScorePaddingObject> paddingObjects = new List<DrawableScorePaddingObject>();
public MyDrawableHitObject(MyHitObject hitObject)
: base(hitObject)
{
}
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);
}
```