// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { /// /// A that matches legacy drain rate calculations as best as possible. /// public abstract partial class LegacyDrainingHealthProcessor : DrainingHealthProcessor { public Action? OnIterationFail; public Action? OnIterationSuccess; protected double HpMultiplierNormal { get; private set; } private double lowestHpEver; private double lowestHpEnd; private double hpRecoveryAvailable; protected LegacyDrainingHealthProcessor(double drainStartTime) : base(drainStartTime) { } public override void ApplyBeatmap(IBeatmap beatmap) { lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); base.ApplyBeatmap(beatmap); } protected override void Reset(bool storeResults) { HpMultiplierNormal = 1; base.Reset(storeResults); } protected override double ComputeDrainRate() { double testDrop = 0.00025; double currentHp; double currentHpUncapped; while (true) { currentHp = 1; currentHpUncapped = 1; double lowestHp = currentHp; double lastTime = DrainStartTime; int currentBreak = 0; bool fail = false; int topLevelObjectCount = 0; foreach (var h in EnumerateTopLevelHitObjects()) { topLevelObjectCount++; while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) { // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, // but this shouldn't have a noticeable impact in practice. lastTime = h.StartTime; currentBreak++; } reduceHp(testDrop * (h.StartTime - lastTime)); lastTime = h.GetEndTime(); if (currentHp < lowestHp) lowestHp = currentHp; if (currentHp <= lowestHpEver) { fail = true; testDrop *= 0.96; OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); break; } double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); double hpOverkill = Math.Max(0, hpReduction - currentHp); reduceHp(hpReduction); foreach (var nested in EnumerateNestedHitObjects(h)) increaseHp(nested); // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners // will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version. if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) { fail = true; testDrop *= 0.96; OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"); break; } increaseHp(h); } if (topLevelObjectCount == 0) return testDrop; if (!fail && currentHp < lowestHpEnd) { fail = true; testDrop *= 0.94; HpMultiplierNormal *= 1.01; OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); } double recovery = (currentHpUncapped - 1) / Math.Max(1, topLevelObjectCount); if (!fail && recovery < hpRecoveryAvailable) { fail = true; testDrop *= 0.96; HpMultiplierNormal *= 1.01; OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); } if (!fail && double.IsInfinity(HpMultiplierNormal)) { OnIterationSuccess?.Invoke("Drain computation algorithm diverged to infinity. PASSING with zero drop, resetting HP multiplier to 1."); HpMultiplierNormal = 1; return 0; } if (!fail) { OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); return testDrop; } } void reduceHp(double amount) { currentHpUncapped = Math.Max(0, currentHpUncapped - amount); currentHp = Math.Max(0, currentHp - amount); } void increaseHp(HitObject hitObject) { double amount = GetHealthIncreaseFor(hitObject, hitObject.Judgement.MaxResult); currentHpUncapped += amount; currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); } } protected sealed override double GetHealthIncreaseFor(JudgementResult result) => GetHealthIncreaseFor(result.HitObject, result.Type); protected abstract IEnumerable EnumerateTopLevelHitObjects(); protected abstract IEnumerable EnumerateNestedHitObjects(HitObject hitObject); protected abstract double GetHealthIncreaseFor(HitObject hitObject, HitResult result); } }