From 62a04a93c837db0aef81557585120877f513660c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Nov 2023 16:22:34 +0900 Subject: [PATCH] Implement legacy catch health processor --- .../Scoring/CatchHealthProcessor.cs | 15 ++ .../Scoring/LegacyCatchHealthProcessor.cs | 237 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs create mode 100644 osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs new file mode 100644 index 0000000000..7e8162bdfa --- /dev/null +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Scoring +{ + public partial class CatchHealthProcessor : DrainingHealthProcessor + { + public CatchHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs new file mode 100644 index 0000000000..ef13ba222c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs @@ -0,0 +1,237 @@ +// 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 System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Scoring +{ + /// + /// Reference implementation for osu!stable's HP drain. + /// Cannot be used for gameplay. + /// + public partial class LegacyCatchHealthProcessor : DrainingHealthProcessor + { + private const double hp_bar_maximum = 200; + private const double hp_combo_geki = 14; + private const double hp_hit_300 = 6; + private const double hp_slider_tick = 3; + + public Action? OnIterationFail; + public Action? OnIterationSuccess; + public bool ApplyComboEndBonus { get; set; } = true; + + private double lowestHpEver; + private double lowestHpEnd; + private double lowestHpComboEnd; + private double hpRecoveryAvailable; + private double hpMultiplierNormal; + private double hpMultiplierComboEnd; + + public LegacyCatchHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); + lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void ApplyResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); + } + + protected override void RevertResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); + } + + protected override void Reset(bool storeResults) + { + hpMultiplierNormal = 1; + hpMultiplierComboEnd = 1; + + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.05; + double currentHp; + double currentHpUncapped; + + List<(HitObject hitObject, bool newCombo)> allObjects = enumerateHitObjects(Beatmap).Where(h => h.hitObject is Fruit || h.hitObject is Droplet || h.hitObject is Banana).ToList(); + + do + { + currentHp = hp_bar_maximum; + currentHpUncapped = hp_bar_maximum; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + int comboTooLowCount = 0; + string failReason = string.Empty; + + for (int i = 0; i < allObjects.Count; i++) + { + HitObject h = allObjects[i].hitObject; + + // Find active break (between current and lastTime) + double localLastTime = lastTime; + double breakTime = 0; + + // Subtract any break time from the duration since the last object + if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + { + BreakPeriod e = Beatmap.Breaks[currentBreak]; + + if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) + { + // consider break start equal to object end time for version 8+ since drain stops during this time + breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; + currentBreak++; + } + } + + reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; + break; + } + + switch (h) + { + case Fruit: + if (ApplyComboEndBonus && (i == allObjects.Count - 1 || allObjects[i + 1].newCombo)) + { + increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); + + if (currentHp < lowestHpComboEnd) + { + if (++comboTooLowCount > 2) + { + hpMultiplierComboEnd *= 1.07; + hpMultiplierNormal *= 1.03; + fail = true; + failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; + } + } + } + else + increaseHp(hpMultiplierNormal * hp_hit_300); + + break; + + case Banana: + increaseHp(hpMultiplierNormal / 2); + break; + + case TinyDroplet: + increaseHp(hpMultiplierNormal * hp_slider_tick * 0.1); + break; + + case Droplet: + increaseHp(hpMultiplierNormal * hp_slider_tick); + break; + } + + if (fail) + break; + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + hpMultiplierComboEnd *= 1.01; + hpMultiplierNormal *= 1.01; + failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; + } + + double recovery = (currentHpUncapped - hp_bar_maximum) / allObjects.Count; + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + hpMultiplierComboEnd *= 1.02; + hpMultiplierNormal *= 1.01; + failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; + } + + if (fail) + { + OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); + continue; + } + + OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); + return testDrop / hp_bar_maximum; + } while (true); + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(double amount) + { + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); + } + } + + private IEnumerable<(HitObject hitObject, bool newCombo)> enumerateHitObjects(IBeatmap beatmap) + { + return enumerateRecursively(beatmap.HitObjects); + + static IEnumerable<(HitObject hitObject, bool newCombo)> enumerateRecursively(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + // The combo end will either be attached to the hitobject itself if it has no children, or the very first child if it has children. + bool newCombo = (hitObject as IHasComboInformation)?.NewCombo ?? false; + + foreach ((HitObject nested, bool _) in enumerateRecursively(hitObject.NestedHitObjects)) + { + yield return (nested, newCombo); + + // Since the combo was attached to the first child, don't attach it to any other child or the parenting hitobject itself. + newCombo = false; + } + + yield return (hitObject, newCombo); + } + } + } + } +}