mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 00:43:22 +08:00
Merge branch 'master' into menu-skin-editor-button
This commit is contained in:
commit
de1d921383
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1121.1" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1124.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||||
|
|
||||||
|
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new CatchHealthProcessor(drainStartTime);
|
||||||
|
|
||||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);
|
||||||
|
|
||||||
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
|
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
|
||||||
|
168
osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
Normal file
168
osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Scoring
|
||||||
|
{
|
||||||
|
public partial class CatchHealthProcessor : DrainingHealthProcessor
|
||||||
|
{
|
||||||
|
public Action<string>? OnIterationFail;
|
||||||
|
public Action<string>? OnIterationSuccess;
|
||||||
|
|
||||||
|
private double lowestHpEver;
|
||||||
|
private double lowestHpEnd;
|
||||||
|
private double hpRecoveryAvailable;
|
||||||
|
private double hpMultiplierNormal;
|
||||||
|
|
||||||
|
public CatchHealthProcessor(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;
|
||||||
|
|
||||||
|
List<HitObject> allObjects = EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana).ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < allObjects.Count; i++)
|
||||||
|
{
|
||||||
|
HitObject h = allObjects[i];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseHp(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) / allObjects.Count;
|
||||||
|
|
||||||
|
if (!fail && recovery < hpRecoveryAvailable)
|
||||||
|
{
|
||||||
|
fail = true;
|
||||||
|
testDrop *= 0.96;
|
||||||
|
hpMultiplierNormal *= 1.01;
|
||||||
|
OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = healthIncreaseFor(hitObject.CreateJudgement().MaxResult);
|
||||||
|
currentHpUncapped += amount;
|
||||||
|
currentHp = Math.Max(0, Math.Min(1, currentHp + amount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type);
|
||||||
|
|
||||||
|
private double healthIncreaseFor(HitResult result)
|
||||||
|
{
|
||||||
|
double increase = 0;
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case HitResult.SmallTickMiss:
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case HitResult.LargeTickMiss:
|
||||||
|
case HitResult.Miss:
|
||||||
|
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
|
||||||
|
|
||||||
|
case HitResult.SmallTickHit:
|
||||||
|
increase = 0.0015;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.LargeTickHit:
|
||||||
|
increase = 0.015;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.Great:
|
||||||
|
increase = 0.03;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitResult.LargeBonus:
|
||||||
|
increase = 0.0025;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hpMultiplierNormal * increase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs
Normal file
93
osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSceneOsuHealthProcessor
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestNoBreak()
|
||||||
|
{
|
||||||
|
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 2000 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(1.4E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleBreak()
|
||||||
|
{
|
||||||
|
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 2000 }
|
||||||
|
},
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(500, 1500)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingBreak()
|
||||||
|
{
|
||||||
|
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 2000 }
|
||||||
|
},
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(500, 1400),
|
||||||
|
new BreakPeriod(750, 1500),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSequentialBreak()
|
||||||
|
{
|
||||||
|
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0 },
|
||||||
|
new HitCircle { StartTime = 2000 }
|
||||||
|
},
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(500, 1000),
|
||||||
|
new BreakPeriod(1000, 1500),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,215 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Beatmaps.Timing;
|
|
||||||
using osu.Game.Rulesets.Judgements;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Scoring
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Reference implementation for osu!stable's HP drain.
|
|
||||||
/// Cannot be used for gameplay.
|
|
||||||
/// </summary>
|
|
||||||
public partial class LegacyOsuHealthProcessor : 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_repeat = 4;
|
|
||||||
private const double hp_slider_tick = 3;
|
|
||||||
|
|
||||||
public Action<string>? OnIterationFail;
|
|
||||||
public Action<string>? 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 LegacyOsuHealthProcessor(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 osu! health processor is not supported for gameplay.");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void RevertResultInternal(JudgementResult result)
|
|
||||||
{
|
|
||||||
if (!IsSimulating)
|
|
||||||
throw new NotSupportedException("The legacy osu! 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;
|
|
||||||
|
|
||||||
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 < Beatmap.HitObjects.Count; i++)
|
|
||||||
{
|
|
||||||
HitObject h = Beatmap.HitObjects[i];
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
double hpReduction = testDrop * (h.GetEndTime() - h.StartTime);
|
|
||||||
double hpOverkill = Math.Max(0, hpReduction - currentHp);
|
|
||||||
reduceHp(hpReduction);
|
|
||||||
|
|
||||||
if (h is Slider slider)
|
|
||||||
{
|
|
||||||
for (int j = 0; j < slider.RepeatCount + 2; j++)
|
|
||||||
increaseHp(hpMultiplierNormal * hp_slider_repeat);
|
|
||||||
foreach (var _ in slider.NestedHitObjects.OfType<SliderTick>())
|
|
||||||
increaseHp(hpMultiplierNormal * hp_slider_tick);
|
|
||||||
}
|
|
||||||
else if (h is Spinner spinner)
|
|
||||||
{
|
|
||||||
foreach (var _ in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
|
|
||||||
increaseHp(hpMultiplierNormal * 1.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver)
|
|
||||||
{
|
|
||||||
fail = true;
|
|
||||||
testDrop *= 0.96;
|
|
||||||
failReason = $"overkill ({currentHp / hp_bar_maximum} - {hpOverkill / hp_bar_maximum} <= {lowestHpEver / hp_bar_maximum})";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ApplyComboEndBonus && (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[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})";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
increaseHp(hpMultiplierNormal * hp_hit_300);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) / Beatmap.HitObjects.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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Timing;
|
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -62,26 +61,16 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
|||||||
{
|
{
|
||||||
HitObject h = Beatmap.HitObjects[i];
|
HitObject h = Beatmap.HitObjects[i];
|
||||||
|
|
||||||
// Find active break (between current and lastTime)
|
while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime)
|
||||||
double localLastTime = lastTime;
|
|
||||||
double breakTime = 0;
|
|
||||||
|
|
||||||
// TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614).
|
|
||||||
// Subtract any break time from the duration since the last object
|
|
||||||
// Note that this method is a bit convoluted, but matches stable code for compatibility.
|
|
||||||
if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count)
|
|
||||||
{
|
{
|
||||||
BreakPeriod e = Beatmap.Breaks[currentBreak];
|
// 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,
|
||||||
if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime)
|
// but this shouldn't have a noticeable impact in practice.
|
||||||
{
|
lastTime = 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++;
|
currentBreak++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
reduceHp(testDrop * (h.StartTime - lastTime - breakTime));
|
reduceHp(testDrop * (h.StartTime - lastTime));
|
||||||
|
|
||||||
lastTime = h.GetEndTime();
|
lastTime = h.GetEndTime();
|
||||||
|
|
||||||
|
@ -192,7 +192,8 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
|
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
|
||||||
AddAssert("not failed", () => !processor.HasFailed);
|
AddAssert("not failed", () => !processor.HasFailed);
|
||||||
|
|
||||||
AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
|
AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result",
|
||||||
|
() => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
|
||||||
AddAssert("failed", () => processor.HasFailed);
|
AddAssert("failed", () => processor.HasFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +233,84 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
assertHealthEqualTo(1);
|
assertHealthEqualTo(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoBreakDrainRate()
|
||||||
|
{
|
||||||
|
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new JudgeableHitObject { StartTime = 0 },
|
||||||
|
new JudgeableHitObject { StartTime = 2000 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(4.5E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleBreakDrainRate()
|
||||||
|
{
|
||||||
|
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new JudgeableHitObject { StartTime = 0 },
|
||||||
|
new JudgeableHitObject { StartTime = 2000 }
|
||||||
|
},
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(500, 1500)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingBreakDrainRate()
|
||||||
|
{
|
||||||
|
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new JudgeableHitObject { StartTime = 0 },
|
||||||
|
new JudgeableHitObject { StartTime = 2000 }
|
||||||
|
},
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(500, 1400),
|
||||||
|
new BreakPeriod(750, 1500),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSequentialBreakDrainRate()
|
||||||
|
{
|
||||||
|
DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000);
|
||||||
|
hp.ApplyBeatmap(new Beatmap<JudgeableHitObject>
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new JudgeableHitObject { StartTime = 0 },
|
||||||
|
new JudgeableHitObject { StartTime = 2000 }
|
||||||
|
},
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(500, 1000),
|
||||||
|
new BreakPeriod(1000, 1500),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5));
|
||||||
|
}
|
||||||
|
|
||||||
private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks)
|
private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks)
|
||||||
{
|
{
|
||||||
var beatmap = new Beatmap
|
var beatmap = new Beatmap
|
||||||
|
@ -103,7 +103,9 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
if (beatmap.HitObjects.Count > 0)
|
if (beatmap.HitObjects.Count > 0)
|
||||||
gameplayEndTime = beatmap.HitObjects[^1].GetEndTime();
|
gameplayEndTime = beatmap.HitObjects[^1].GetEndTime();
|
||||||
|
|
||||||
noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period(
|
noDrainPeriodTracker = new PeriodTracker(
|
||||||
|
beatmap.Breaks.Select(breakPeriod =>
|
||||||
|
new Period(
|
||||||
beatmap.HitObjects
|
beatmap.HitObjects
|
||||||
.Select(hitObject => hitObject.GetEndTime())
|
.Select(hitObject => hitObject.GetEndTime())
|
||||||
.Where(endTime => endTime <= breakPeriod.StartTime)
|
.Where(endTime => endTime <= breakPeriod.StartTime)
|
||||||
@ -159,26 +161,24 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
{
|
{
|
||||||
double currentHealth = 1;
|
double currentHealth = 1;
|
||||||
double lowestHealth = 1;
|
double lowestHealth = 1;
|
||||||
int currentBreak = -1;
|
int currentBreak = 0;
|
||||||
|
|
||||||
for (int i = 0; i < healthIncreases.Count; i++)
|
for (int i = 0; i < healthIncreases.Count; i++)
|
||||||
{
|
{
|
||||||
double currentTime = healthIncreases[i].time;
|
double currentTime = healthIncreases[i].time;
|
||||||
double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime;
|
double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime;
|
||||||
|
|
||||||
// Subtract any break time from the duration since the last object
|
while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime)
|
||||||
if (Beatmap.Breaks.Count > 0)
|
|
||||||
{
|
{
|
||||||
// Advance the last break occuring before the current time
|
// If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects.
|
||||||
while (currentBreak + 1 < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak + 1].EndTime < currentTime)
|
// 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 = currentTime;
|
||||||
currentBreak++;
|
currentBreak++;
|
||||||
|
|
||||||
if (currentBreak >= 0)
|
|
||||||
lastTime = Math.Max(lastTime, Beatmap.Breaks[currentBreak].EndTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply health adjustments
|
// Apply health adjustments
|
||||||
currentHealth -= (healthIncreases[i].time - lastTime) * result;
|
currentHealth -= (currentTime - lastTime) * result;
|
||||||
lowestHealth = Math.Min(lowestHealth, currentHealth);
|
lowestHealth = Math.Min(lowestHealth, currentHealth);
|
||||||
currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health);
|
currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health);
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="11.5.0" />
|
<PackageReference Include="Realm" Version="11.5.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1121.1" />
|
<PackageReference Include="ppy.osu.Framework" Version="2023.1124.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1114.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1114.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.40.0" />
|
<PackageReference Include="Sentry" Version="3.40.0" />
|
||||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||||
|
@ -23,6 +23,6 @@
|
|||||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1121.1" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1124.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Loading…
Reference in New Issue
Block a user