From 322a78830eadd6045d5a989057d8d209a1621d12 Mon Sep 17 00:00:00 2001 From: smoogipooo Date: Thu, 23 Mar 2017 19:24:23 +0900 Subject: [PATCH] Implement Taiko score processing. --- osu.Game.Modes.Taiko/TaikoScoreProcessor.cs | 244 ++++++++++++++++++++ osu.Game/Modes/ScoreProcessor.cs | 4 +- 2 files changed, 246 insertions(+), 2 deletions(-) diff --git a/osu.Game.Modes.Taiko/TaikoScoreProcessor.cs b/osu.Game.Modes.Taiko/TaikoScoreProcessor.cs index 849c0fa894..0beb9f7418 100644 --- a/osu.Game.Modes.Taiko/TaikoScoreProcessor.cs +++ b/osu.Game.Modes.Taiko/TaikoScoreProcessor.cs @@ -1,14 +1,120 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using OpenTK; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.Taiko.Judgements; using osu.Game.Modes.Taiko.Objects; using osu.Game.Modes.UI; +using System; namespace osu.Game.Modes.Taiko { internal class TaikoScoreProcessor : ScoreProcessor { + /// + /// The maximum score achievable. + /// Does _not_ include bonus score - for bonus score see . + /// + private const int max_score = 1000000; + + /// + /// The amount of the score attributed to combo. + /// + private const double combo_portion_max = max_score * 0.2; + + /// + /// The amount of the score attributed to accuracy. + /// + private const double accuracy_portion_max = max_score * 0.8; + + /// + /// The factor used to determine relevance of combos. + /// + private const double combo_base = 4; + + /// + /// The HP awarded by a hit. + /// + private const double hp_hit_great = 0.03; + + /// + /// The HP awarded for a hit at HP >= 5. + /// + private const double hp_hit_good = 0.011; + + /// + /// The HP awarded for a hit at HP = 0. + /// + /// Yes, this is incorrect, and goods at HP = 0 will award more HP than greats. + /// This is legacy and should be fixed, but is kept as is for now for compatibility. + /// + /// + private const double hp_hit_good_max = hp_hit_good * 8; + + /// + /// The HP deducted for a at HP = 0. + /// + private const double hp_miss_min = -0.0018; + + /// + /// The HP deducted for a at HP = 5. + /// + private const double hp_miss_mid = -0.0075; + + /// + /// The HP deducted for a at HP = 10. + /// + private const double hp_miss_max = -0.12; + + /// + /// The HP awarded for a hit. + /// + /// hits award less HP as they're more spammable, although in hindsight + /// this probably awards too little HP and is kept at this value for now for compatibility. + /// + /// + private const double hp_hit_tick = 0.00000003; + + /// + /// Taiko fails at the end of the map if the player has not half-filled their HP bar. + /// + public override bool HasFailed => totalHits == maxTotalHits && Health.Value <= 0.5; + + /// + /// The final combo portion of the score. + /// + private double comboScore => combo_portion_max * comboPortion / maxComboPortion; + + /// + /// The final accuracy portion of the score. + /// + private double accuracyScore => accuracy_portion_max * Math.Pow(Accuracy, 3.6) * totalHits / maxTotalHits; + + /// + /// The final bonus score. + /// This is added on top of , thus the total score can exceed . + /// + private double bonusScore; + + /// + /// The multiple of the original score added to the combo portion of the score + /// for correctly hitting a finisher with both keys. + /// + private double finisherScoreScale; + + private double hpIncreaseTick; + private double hpIncreaseGreat; + private double hpIncreaseGood; + private double hpIncreaseMiss; + + private double maxComboPortion; + private double comboPortion; + private int maxTotalHits; + private int totalHits; + public TaikoScoreProcessor() { } @@ -18,8 +124,146 @@ namespace osu.Game.Modes.Taiko { } + protected override void ComputeTargets(Beatmap beatmap) + { + double hpMultiplierNormal = 1 / (hp_hit_great * beatmap.HitObjects.FindAll(o => o is Hit).Count * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, 0.5, 0.75, 0.98)); + + hpIncreaseTick = hp_hit_tick; + hpIncreaseGreat = hpMultiplierNormal * hp_hit_great; + hpIncreaseGood = hpMultiplierNormal * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, hp_hit_good_max, hp_hit_good, hp_hit_good); + hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max); + + var finishers = beatmap.HitObjects.FindAll(o => o is Hit && o.IsFinisher); + + // This is a linear function that awards: + // 10 times bonus points for hitting a finisher with both keys with 30 finishers in the map + // 3 times bonus points for hitting a finisher with both keys with 120 finishers in the map + finisherScoreScale = -7d / 90d * MathHelper.Clamp(finishers.Count, 30, 120) + 111d / 9d; + + foreach (TaikoHitObject obj in beatmap.HitObjects) + { + if (obj is Hit) + { + AddJudgement(new TaikoJudgementInfo + { + Result = HitResult.Hit, + TaikoResult = TaikoHitResult.Great, + SecondHit = obj.IsFinisher + }); + } + else if (obj is DrumRoll) + { + for (int i = 0; i < ((DrumRoll)obj).TotalTicks; i++) + { + AddJudgement(new TaikoDrumRollTickJudgementInfo + { + Result = HitResult.Hit, + TaikoResult = TaikoHitResult.Great, + SecondHit = obj.IsFinisher + }); + } + + AddJudgement(new TaikoJudgementInfo + { + Result = HitResult.Hit, + TaikoResult = TaikoHitResult.Great, + SecondHit = obj.IsFinisher + }); + } + else if (obj is Bash) + { + AddJudgement(new TaikoJudgementInfo + { + Result = HitResult.Hit, + TaikoResult = TaikoHitResult.Great + }); + } + } + + maxTotalHits = totalHits; + maxComboPortion = comboPortion; + } + protected override void UpdateCalculations(TaikoJudgementInfo newJudgement) { + TaikoDrumRollTickJudgementInfo tickJudgement = newJudgement as TaikoDrumRollTickJudgementInfo; + + // Don't consider ticks as a type of hit that counts towards map completion + if (tickJudgement == null) + totalHits++; + + // Apply score changes + if (newJudgement.Result == HitResult.Hit) + { + double baseValue = newJudgement.ScoreValue; + + // Add bonus points for hitting a finisher with the second key + if (newJudgement.SecondHit) + baseValue += baseValue * finisherScoreScale; + + // Add score to portions + if (tickJudgement != null) + bonusScore += baseValue; + else + { + Combo.Value++; + + // A relevance factor that needs to be applied to make higher combos more relevant + // Value is capped at 400 combo + double comboRelevance = Math.Min(Math.Log(400, combo_base), Math.Max(0.5, Math.Log(Combo.Value, combo_base))); + + comboPortion += baseValue * comboRelevance; + } + } + + // Apply HP changes + switch (newJudgement.Result) + { + case HitResult.Miss: + // Missing ticks shouldn't drop HP + if (tickJudgement == null) + Health.Value += hpIncreaseMiss; + break; + case HitResult.Hit: + switch (newJudgement.TaikoResult) + { + case TaikoHitResult.Good: + Health.Value += hpIncreaseGood; + break; + case TaikoHitResult.Great: + // Ticks only give out a different portion of HP because they're more spammable + if (tickJudgement != null) + Health.Value += hpIncreaseTick; + else + Health.Value += hpIncreaseGreat; + break; + } + break; + } + + // Compute the new score + accuracy + int score = 0; + int maxScore = 0; + + foreach (var j in Judgements) + { + score += j.AccuracyScoreValue; + maxScore = j.MaxAccuracyScoreValue; + } + + Accuracy.Value = (double)score / maxScore; + TotalScore.Value = comboScore + accuracyScore + bonusScore; + } + + protected override void Reset() + { + base.Reset(); + + Health.Value = 0; + + bonusScore = 0; + comboPortion = 0; + totalHits = 0; } } } diff --git a/osu.Game/Modes/ScoreProcessor.cs b/osu.Game/Modes/ScoreProcessor.cs index a6e29d53e1..05e7647126 100644 --- a/osu.Game/Modes/ScoreProcessor.cs +++ b/osu.Game/Modes/ScoreProcessor.cs @@ -122,7 +122,7 @@ namespace osu.Game.Modes { Judgements.Capacity = hitRenderer.Beatmap.HitObjects.Count; - hitRenderer.OnJudgement += addJudgement; + hitRenderer.OnJudgement += AddJudgement; ComputeTargets(hitRenderer.Beatmap); @@ -139,7 +139,7 @@ namespace osu.Game.Modes /// Adds a judgement to this ScoreProcessor. /// /// The judgement to add. - private void addJudgement(TJudgement judgement) + protected void AddJudgement(TJudgement judgement) { Judgements.Add(judgement);