mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 15:22:55 +08:00
Merge branch 'master' into velocity-based-ball-animation
This commit is contained in:
commit
23ea128f30
@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Catch.UI;
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
||||||
|
|
||||||
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
|
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.Catch.Objects;
|
|||||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Catch.Skinning;
|
using osu.Game.Rulesets.Catch.Skinning;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -182,11 +183,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Drawable CreateProxiedContent() => caughtObjectContainer.CreateProxy();
|
public Drawable CreateProxiedContent() => caughtObjectContainer.CreateProxy();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
|
||||||
/// </summary>
|
|
||||||
private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -471,6 +467,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
d.Expire();
|
d.Expire();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize) * 2);
|
||||||
|
|
||||||
private enum DroppedObjectAnimation
|
private enum DroppedObjectAnimation
|
||||||
{
|
{
|
||||||
Drop,
|
Drop,
|
||||||
|
@ -108,7 +108,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
RelativeSizeAxes = Axes.X
|
RelativeSizeAxes = Axes.X
|
||||||
},
|
},
|
||||||
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
||||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
slidingSample = new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
Looping = true,
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
maskedContents.AddRange(new[]
|
maskedContents.AddRange(new[]
|
||||||
|
@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
||||||
|
|
||||||
[TestCase(6.7115569159190587d, 206, "diffcalc-test")]
|
[TestCase(6.710442985146793d, 206, "diffcalc-test")]
|
||||||
[TestCase(1.4391311903612753d, 45, "zero-length-sliders")]
|
[TestCase(1.4386882251130073d, 45, "zero-length-sliders")]
|
||||||
[TestCase(0.42506480230838789d, 2, "very-fast-slider")]
|
[TestCase(0.42506480230838789d, 2, "very-fast-slider")]
|
||||||
[TestCase(0.14102693012101306d, 1, "nan-slider")]
|
[TestCase(0.14102693012101306d, 1, "nan-slider")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(8.9757300665532966d, 206, "diffcalc-test")]
|
[TestCase(8.9742952703071666d, 206, "diffcalc-test")]
|
||||||
[TestCase(0.55071082800473514d, 2, "very-fast-slider")]
|
[TestCase(0.55071082800473514d, 2, "very-fast-slider")]
|
||||||
[TestCase(1.7437232654020756d, 45, "zero-length-sliders")]
|
[TestCase(1.743180218215227d, 45, "zero-length-sliders")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
[TestCase(6.7115569159190587d, 239, "diffcalc-test")]
|
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
||||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
||||||
[TestCase(1.4391311903612753d, 54, "zero-length-sliders")]
|
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
||||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||||
|
|
||||||
|
135
osu.Game.Rulesets.Osu.Tests/SpinnerSpinHistoryTest.cs
Normal file
135
osu.Game.Rulesets.Osu.Tests/SpinnerSpinHistoryTest.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// 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.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SpinnerSpinHistoryTest
|
||||||
|
{
|
||||||
|
private SpinnerSpinHistory history = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
history = new SpinnerSpinHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0, 0)]
|
||||||
|
[TestCase(10, 10)]
|
||||||
|
[TestCase(180, 180)]
|
||||||
|
[TestCase(350, 350)]
|
||||||
|
[TestCase(360, 360)]
|
||||||
|
[TestCase(370, 370)]
|
||||||
|
[TestCase(540, 540)]
|
||||||
|
[TestCase(720, 720)]
|
||||||
|
// ---
|
||||||
|
[TestCase(-0, 0)]
|
||||||
|
[TestCase(-10, 10)]
|
||||||
|
[TestCase(-180, 180)]
|
||||||
|
[TestCase(-350, 350)]
|
||||||
|
[TestCase(-360, 360)]
|
||||||
|
[TestCase(-370, 370)]
|
||||||
|
[TestCase(-540, 540)]
|
||||||
|
[TestCase(-720, 720)]
|
||||||
|
public void TestSpinOneDirection(float spin, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(500, spin);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(0, 0, 0, 0)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -10, 0, 10)]
|
||||||
|
[TestCase(-10, 10, 0, 10)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -20, 0, 10)]
|
||||||
|
[TestCase(-10, 20, 0, 10)]
|
||||||
|
// ---
|
||||||
|
[TestCase(20, -10, 0, 20)]
|
||||||
|
[TestCase(-20, 10, 0, 20)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -360, 0, 350)]
|
||||||
|
[TestCase(-10, 360, 0, 350)]
|
||||||
|
// ---
|
||||||
|
[TestCase(360, -10, 0, 370)]
|
||||||
|
[TestCase(360, 10, 0, 370)]
|
||||||
|
[TestCase(-360, 10, 0, 370)]
|
||||||
|
[TestCase(-360, -10, 0, 370)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, 10, 10, 30)]
|
||||||
|
[TestCase(10, 10, -10, 20)]
|
||||||
|
[TestCase(10, -10, 10, 10)]
|
||||||
|
[TestCase(-10, -10, -10, 30)]
|
||||||
|
[TestCase(-10, -10, 10, 20)]
|
||||||
|
[TestCase(-10, 10, 10, 10)]
|
||||||
|
// ---
|
||||||
|
[TestCase(10, -20, -350, 360)]
|
||||||
|
[TestCase(10, -20, 350, 340)]
|
||||||
|
[TestCase(-10, 20, 350, 360)]
|
||||||
|
[TestCase(-10, 20, -350, 340)]
|
||||||
|
public void TestSpinMultipleDirections(float spin1, float spin2, float spin3, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(500, spin1);
|
||||||
|
history.ReportDelta(1000, spin2);
|
||||||
|
history.ReportDelta(1500, spin3);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One spin
|
||||||
|
[TestCase(370, -50, 320)]
|
||||||
|
[TestCase(-370, 50, 320)]
|
||||||
|
// Two spins
|
||||||
|
[TestCase(740, -420, 320)]
|
||||||
|
[TestCase(-740, 420, 320)]
|
||||||
|
public void TestRemoveAndCrossFullSpin(float deltaToAdd, float deltaToRemove, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(1000, deltaToAdd);
|
||||||
|
history.ReportDelta(500, deltaToRemove);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One spin + partial
|
||||||
|
[TestCase(400, -30, -50, 320)]
|
||||||
|
[TestCase(-400, 30, 50, 320)]
|
||||||
|
// Two spins + partial
|
||||||
|
[TestCase(800, -430, -50, 320)]
|
||||||
|
[TestCase(-800, 430, 50, 320)]
|
||||||
|
public void TestRemoveAndCrossFullAndPartialSpins(float deltaToAdd1, float deltaToAdd2, float deltaToRemove, float expectedRotation)
|
||||||
|
{
|
||||||
|
history.ReportDelta(1000, deltaToAdd1);
|
||||||
|
history.ReportDelta(1500, deltaToAdd2);
|
||||||
|
history.ReportDelta(500, deltaToRemove);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRewindMultipleFullSpins()
|
||||||
|
{
|
||||||
|
history.ReportDelta(500, 360);
|
||||||
|
history.ReportDelta(1000, 720);
|
||||||
|
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(1080));
|
||||||
|
|
||||||
|
history.ReportDelta(250, -900);
|
||||||
|
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(180));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRewindOverDirectionChange()
|
||||||
|
{
|
||||||
|
history.ReportDelta(1000, 40); // max is now CW 40 degrees
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(40));
|
||||||
|
history.ReportDelta(1100, -90); // max is now CCW 50 degrees
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(50));
|
||||||
|
history.ReportDelta(1200, 110); // max is now CW 60 degrees
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(60));
|
||||||
|
|
||||||
|
history.ReportDelta(1000, -20);
|
||||||
|
Assert.That(history.TotalRotation, Is.EqualTo(40));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
const double time_slider_start = 1000;
|
const double time_slider_start = 1000;
|
||||||
|
|
||||||
float circleRadius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (circleSize - 5) / 5) / 2;
|
float circleRadius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(circleSize, true);
|
||||||
float followCircleRadius = circleRadius * 1.2f;
|
float followCircleRadius = circleRadius * 1.2f;
|
||||||
|
|
||||||
performTest(new Beatmap<OsuHitObject>
|
performTest(new Beatmap<OsuHitObject>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -26,6 +27,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
private TestDrawableSpinner drawableSpinner;
|
private TestDrawableSpinner drawableSpinner;
|
||||||
|
|
||||||
|
private readonly BindableDouble spinRate = new BindableDouble();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AddSliderStep("Spin rate", 0.5, 5, 1, val => spinRate.Value = val);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(true)]
|
[TestCase(true)]
|
||||||
[TestCase(false)]
|
[TestCase(false)]
|
||||||
public void TestVariousSpinners(bool autoplay)
|
public void TestVariousSpinners(bool autoplay)
|
||||||
@ -86,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = od });
|
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = od });
|
||||||
|
|
||||||
return drawableSpinner = new TestDrawableSpinner(spinner, true)
|
return drawableSpinner = new TestDrawableSpinner(spinner, true, spinRate)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Depth = depthIndex++,
|
Depth = depthIndex++,
|
||||||
@ -114,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
|
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
|
||||||
|
|
||||||
drawableSpinner = new TestDrawableSpinner(spinner, auto)
|
drawableSpinner = new TestDrawableSpinner(spinner, auto, spinRate)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Depth = depthIndex++,
|
Depth = depthIndex++,
|
||||||
@ -130,18 +140,20 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private partial class TestDrawableSpinner : DrawableSpinner
|
private partial class TestDrawableSpinner : DrawableSpinner
|
||||||
{
|
{
|
||||||
private readonly bool auto;
|
private readonly bool auto;
|
||||||
|
private readonly BindableDouble spinRate;
|
||||||
|
|
||||||
public TestDrawableSpinner(Spinner s, bool auto)
|
public TestDrawableSpinner(Spinner s, bool auto, BindableDouble spinRate)
|
||||||
: base(s)
|
: base(s)
|
||||||
{
|
{
|
||||||
this.auto = auto;
|
this.auto = auto;
|
||||||
|
this.spinRate = spinRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
if (auto)
|
if (auto)
|
||||||
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 2));
|
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * spinRate.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
/// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestVibrateWithoutSpinningOffCentre()
|
public void TestVibrateWithoutSpinningOffCentre()
|
||||||
{
|
{
|
||||||
List<ReplayFrame> frames = new List<ReplayFrame>();
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||||
@ -81,7 +80,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
/// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestVibrateWithoutSpinningOnCentre()
|
public void TestVibrateWithoutSpinningOnCentre()
|
||||||
{
|
{
|
||||||
List<ReplayFrame> frames = new List<ReplayFrame>();
|
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||||
@ -130,7 +128,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
/// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW).
|
/// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestSpinHalfBothDirections()
|
public void TestSpinHalfBothDirections()
|
||||||
{
|
{
|
||||||
performTest(new SpinFramesGenerator(time_spinner_start)
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||||
@ -149,7 +146,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
[TestCase(-180, 540, 1)]
|
[TestCase(-180, 540, 1)]
|
||||||
[TestCase(180, -900, 2)]
|
[TestCase(180, -900, 2)]
|
||||||
[TestCase(-180, 900, 2)]
|
[TestCase(-180, 900, 2)]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks)
|
public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks)
|
||||||
{
|
{
|
||||||
performTest(new SpinFramesGenerator(time_spinner_start)
|
performTest(new SpinFramesGenerator(time_spinner_start)
|
||||||
@ -162,18 +158,28 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Ignore("An upcoming implementation will fix this case")]
|
|
||||||
public void TestRewind()
|
public void TestRewind()
|
||||||
{
|
{
|
||||||
AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 });
|
AddStep("set manual clock", () => manualClock = new ManualClock
|
||||||
|
{
|
||||||
|
// Avoids interpolation trying to run ahead during testing.
|
||||||
|
Rate = 0
|
||||||
|
});
|
||||||
|
|
||||||
List<ReplayFrame> frames = new SpinFramesGenerator(time_spinner_start)
|
List<ReplayFrame> frames =
|
||||||
.Spin(360, 500) // 2000ms -> 1 full CW spin
|
new SpinFramesGenerator(time_spinner_start)
|
||||||
.Spin(-180, 500) // 2500ms -> 0.5 CCW spins
|
// 1500ms start
|
||||||
.Spin(90, 500) // 3000ms -> 0.25 CW spins
|
.Spin(360, 500)
|
||||||
.Spin(450, 500) // 3500ms -> 1 full CW spin
|
// 2000ms -> 1 full CW spin
|
||||||
.Spin(180, 500) // 4000ms -> 0.5 CW spins
|
.Spin(-180, 500)
|
||||||
.Build();
|
// 2500ms -> 1 full CW spin + 0.5 CCW spins
|
||||||
|
.Spin(90, 500)
|
||||||
|
// 3000ms -> 1 full CW spin + 0.25 CCW spins
|
||||||
|
.Spin(450, 500)
|
||||||
|
// 3500ms -> 2 full CW spins
|
||||||
|
.Spin(180, 500)
|
||||||
|
// 4000ms -> 2 full CW spins + 0.5 CW spins
|
||||||
|
.Build();
|
||||||
|
|
||||||
loadPlayer(frames);
|
loadPlayer(frames);
|
||||||
|
|
||||||
@ -190,15 +196,35 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
DrawableSpinner drawableSpinner = null!;
|
DrawableSpinner drawableSpinner = null!;
|
||||||
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().Single()) != null);
|
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().Single()) != null);
|
||||||
|
|
||||||
assertTotalRotation(4000, 900);
|
assertFinalRotationCorrect();
|
||||||
assertTotalRotation(3750, 810);
|
assertTotalRotation(3750, 810);
|
||||||
assertTotalRotation(3500, 720);
|
assertTotalRotation(3500, 720);
|
||||||
assertTotalRotation(3250, 530);
|
assertTotalRotation(3250, 530);
|
||||||
assertTotalRotation(3000, 540);
|
assertTotalRotation(3000, 450);
|
||||||
assertTotalRotation(2750, 540);
|
assertTotalRotation(2750, 540);
|
||||||
assertTotalRotation(2500, 540);
|
assertTotalRotation(2500, 540);
|
||||||
assertTotalRotation(2250, 360);
|
assertTotalRotation(2250, 450);
|
||||||
assertTotalRotation(2000, 180);
|
assertTotalRotation(2000, 360);
|
||||||
|
assertTotalRotation(1500, 0);
|
||||||
|
|
||||||
|
// same thing but always returning to final time to check.
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3750, 810);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3500, 720);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3250, 530);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(3000, 450);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2750, 540);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2500, 540);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2250, 450);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
|
assertTotalRotation(2000, 360);
|
||||||
|
assertFinalRotationCorrect();
|
||||||
assertTotalRotation(1500, 0);
|
assertTotalRotation(1500, 0);
|
||||||
|
|
||||||
void assertTotalRotation(double time, float expected)
|
void assertTotalRotation(double time, float expected)
|
||||||
@ -211,8 +237,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
void addSeekStep(double time)
|
void addSeekStep(double time)
|
||||||
{
|
{
|
||||||
AddStep($"seek to {time}", () => clock.Seek(time));
|
AddStep($"seek to {time}", () => clock.Seek(time));
|
||||||
|
// Lenience is required due to interpolation running slightly ahead on a stalled clock.
|
||||||
AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time));
|
AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void assertFinalRotationCorrect() => assertTotalRotation(4000, 900);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertTicksHit(int count)
|
private void assertTicksHit(int count)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
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;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Judgements
|
namespace osu.Game.Rulesets.Osu.Judgements
|
||||||
{
|
{
|
||||||
@ -15,28 +16,15 @@ namespace osu.Game.Rulesets.Osu.Judgements
|
|||||||
public Spinner Spinner => (Spinner)HitObject;
|
public Spinner Spinner => (Spinner)HitObject;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The total rotation performed on the spinner disc, disregarding the spin direction,
|
/// The total amount that the spinner was rotated.
|
||||||
/// adjusted for the track's playback rate.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
public float TotalRotation => History.TotalRotation;
|
||||||
/// <para>
|
|
||||||
/// This value is always non-negative and is monotonically increasing with time
|
/// <summary>
|
||||||
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
|
/// Stores the spinning history of the spinner.<br />
|
||||||
/// </para>
|
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner.
|
||||||
/// <para>
|
/// </summary>
|
||||||
/// The rotation from each frame is multiplied by the clock's current playback rate.
|
public readonly SpinnerSpinHistory History = new SpinnerSpinHistory();
|
||||||
/// The reason this is done is to ensure that spinners give the same score and require the same number of spins
|
|
||||||
/// regardless of whether speed-modifying mods are applied.
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
/// <example>
|
|
||||||
/// Assuming no speed-modifying mods are active,
|
|
||||||
/// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
|
|
||||||
/// this property will return the value of 720 (as opposed to 0).
|
|
||||||
/// If Double Time is active instead (with a speed multiplier of 1.5x),
|
|
||||||
/// in the same scenario the property will return 720 * 1.5 = 1080.
|
|
||||||
/// </example>
|
|
||||||
public float TotalRotation;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Time instant at which the spin was started (the first user input which caused an increase in spin).
|
/// Time instant at which the spin was started (the first user input which caused an increase in spin).
|
||||||
|
@ -107,7 +107,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||||
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
||||||
Ball,
|
Ball,
|
||||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
slidingSample = new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
Looping = true,
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||||
|
@ -48,9 +48,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
|
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IBindable<double> GainedBonus => gainedBonus;
|
public double CurrentBonusScore => score_per_tick * Math.Clamp(completedFullSpins.Value - HitObject.SpinsRequiredForBonus, 0, HitObject.MaximumBonusSpins);
|
||||||
|
|
||||||
private readonly Bindable<double> gainedBonus = new BindableDouble();
|
/// <summary>
|
||||||
|
/// The maximum amount of bonus score which can be achieved from extra spins.
|
||||||
|
/// </summary>
|
||||||
|
public double MaximumBonusScore => score_per_tick * HitObject.MaximumBonusSpins;
|
||||||
|
|
||||||
|
public IBindable<int> CompletedFullSpins => completedFullSpins;
|
||||||
|
|
||||||
|
private readonly Bindable<int> completedFullSpins = new Bindable<int>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of spins per minute this spinner is spinning at, for display purposes.
|
/// The number of spins per minute this spinner is spinning at, for display purposes.
|
||||||
@ -99,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
spinningSample = new PausableSkinnableSound
|
spinningSample = new PausableSkinnableSound
|
||||||
{
|
{
|
||||||
Volume = { Value = 0 },
|
Volume = { Value = 0 },
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||||
Looping = true,
|
Looping = true,
|
||||||
Frequency = { Value = spinning_sample_initial_frequency }
|
Frequency = { Value = spinning_sample_initial_frequency }
|
||||||
}
|
}
|
||||||
@ -286,8 +294,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||||
|
|
||||||
private int completedFullSpins;
|
|
||||||
|
|
||||||
private void updateBonusScore()
|
private void updateBonusScore()
|
||||||
{
|
{
|
||||||
if (ticks.Count == 0)
|
if (ticks.Count == 0)
|
||||||
@ -295,27 +301,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
int spins = (int)(Result.TotalRotation / 360);
|
int spins = (int)(Result.TotalRotation / 360);
|
||||||
|
|
||||||
if (spins < completedFullSpins)
|
if (spins < completedFullSpins.Value)
|
||||||
{
|
{
|
||||||
// rewinding, silently handle
|
// rewinding, silently handle
|
||||||
completedFullSpins = spins;
|
completedFullSpins.Value = spins;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (completedFullSpins != spins)
|
while (completedFullSpins.Value != spins)
|
||||||
{
|
{
|
||||||
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
||||||
|
|
||||||
// tick may be null if we've hit the spin limit.
|
// tick may be null if we've hit the spin limit.
|
||||||
if (tick != null)
|
tick?.TriggerResult(true);
|
||||||
{
|
|
||||||
tick.TriggerResult(true);
|
|
||||||
|
|
||||||
if (tick is DrawableSpinnerBonusTick)
|
completedFullSpins.Value++;
|
||||||
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequiredForBonus);
|
|
||||||
}
|
|
||||||
|
|
||||||
completedFullSpins++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnApply()
|
||||||
|
{
|
||||||
|
base.OnApply();
|
||||||
|
|
||||||
|
// the tick can be theoretically judged at any point in the spinner's duration,
|
||||||
|
// so it must be alive throughout the spinner's entire lifetime.
|
||||||
|
// this mostly matters for correct sample playback.
|
||||||
|
LifetimeStart = DrawableSpinner.HitObject.StartTime;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply a judgement result.
|
/// Apply a judgement result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
146
osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerSpinHistory.cs
Normal file
146
osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerSpinHistory.cs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the spinning history of a single spinner.<br />
|
||||||
|
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A single, full rotation of the spinner is defined as a 360-degree rotation of the spinner, starting from 0, going in a single direction.<br />
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// If the player spins 90-degrees clockwise, then changes direction, they need to spin 90-degrees counter-clockwise to return to 0
|
||||||
|
/// and then continue rotating the spinner for another 360-degrees in the same direction.
|
||||||
|
/// </example>
|
||||||
|
public class SpinnerSpinHistory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The sum of all complete spins and any current partial spin, in degrees.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is the final scoring value.
|
||||||
|
/// </remarks>
|
||||||
|
public float TotalRotation => 360 * completedSpins.Count + currentSpinMaxRotation;
|
||||||
|
|
||||||
|
private readonly Stack<CompletedSpin> completedSpins = new Stack<CompletedSpin>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total accumulated (absolute) rotation.
|
||||||
|
/// </summary>
|
||||||
|
private float totalAccumulatedRotation;
|
||||||
|
|
||||||
|
private float totalAccumulatedRotationAtLastCompletion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For the current spin, represents the maximum absolute rotation (from 0..360) achieved by the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is used to report <see cref="TotalRotation"/> in the case a user spins backwards.
|
||||||
|
/// Basically it allows us to not reduce the total rotation in such a case.
|
||||||
|
///
|
||||||
|
/// This also stops spinner "cheese" where a user may rapidly change directions and cause an increase
|
||||||
|
/// in rotations.
|
||||||
|
/// </remarks>
|
||||||
|
private float currentSpinMaxRotation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current spin, from -360..360.
|
||||||
|
/// </summary>
|
||||||
|
private float currentSpinRotation => totalAccumulatedRotation - totalAccumulatedRotationAtLastCompletion;
|
||||||
|
|
||||||
|
private double lastReportTime = double.NegativeInfinity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Report a delta update based on user input.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentTime">The current time.</param>
|
||||||
|
/// <param name="delta">The delta of the angle moved through since the last report.</param>
|
||||||
|
public void ReportDelta(double currentTime, float delta)
|
||||||
|
{
|
||||||
|
if (delta == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Importantly, outside of tests the max delta entering here is 180 degrees.
|
||||||
|
// If it wasn't for tests, we could add this line:
|
||||||
|
//
|
||||||
|
// Debug.Assert(Math.Abs(delta) < 180);
|
||||||
|
//
|
||||||
|
// For this to be 101% correct, we need to add the ability for important frames to be
|
||||||
|
// created based on gameplay intrinsics (ie. there should be one frame for any spinner delta 90 < n < 180 degrees).
|
||||||
|
//
|
||||||
|
// But this can come later.
|
||||||
|
|
||||||
|
totalAccumulatedRotation += delta;
|
||||||
|
|
||||||
|
if (currentTime >= lastReportTime)
|
||||||
|
{
|
||||||
|
currentSpinMaxRotation = Math.Max(currentSpinMaxRotation, Math.Abs(currentSpinRotation));
|
||||||
|
|
||||||
|
// Handle the case where the user has completed another spin.
|
||||||
|
// Note that this does could be an `if` rather than `while` if the above assertion held true.
|
||||||
|
// It is a `while` loop to handle tests which throw larger values at this method.
|
||||||
|
while (currentSpinMaxRotation >= 360)
|
||||||
|
{
|
||||||
|
int direction = Math.Sign(currentSpinRotation);
|
||||||
|
|
||||||
|
completedSpins.Push(new CompletedSpin(currentTime, direction));
|
||||||
|
|
||||||
|
// Incrementing the last completion point will cause `currentSpinRotation` to
|
||||||
|
// hold the remaining spin that needs to be considered.
|
||||||
|
totalAccumulatedRotationAtLastCompletion += direction * 360;
|
||||||
|
|
||||||
|
// Reset the current max as we are entering a new spin.
|
||||||
|
// Importantly, carry over the remainder (which is now stored in `currentSpinRotation`).
|
||||||
|
currentSpinMaxRotation = Math.Abs(currentSpinRotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// When rewinding, the main thing we care about is getting `totalAbsoluteRotationsAtLastCompletion`
|
||||||
|
// to the correct value. We can used the stored history for this.
|
||||||
|
while (completedSpins.TryPeek(out var segment) && segment.CompletionTime > currentTime)
|
||||||
|
{
|
||||||
|
completedSpins.Pop();
|
||||||
|
totalAccumulatedRotationAtLastCompletion -= segment.Direction * 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a best effort. We may not have enough data to match this 1:1, but that's okay.
|
||||||
|
// We know that the player is somewhere in a spin.
|
||||||
|
// In the worst case, this will be lower than expected, and recover in forward playback.
|
||||||
|
currentSpinMaxRotation = Math.Abs(currentSpinRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReportTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single completed spin.
|
||||||
|
/// </summary>
|
||||||
|
private class CompletedSpin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which this spin completion occurred.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double CompletionTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The direction this spin completed in.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int Direction;
|
||||||
|
|
||||||
|
public CompletedSpin(double completionTime, int direction)
|
||||||
|
{
|
||||||
|
Debug.Assert(direction == -1 || direction == 1);
|
||||||
|
|
||||||
|
CompletionTime = completionTime;
|
||||||
|
Direction = direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -155,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
// This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in.
|
// This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in.
|
||||||
TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN);
|
TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN);
|
||||||
|
|
||||||
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
|
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
||||||
|
@ -35,14 +35,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
bonusCounter = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Alpha = 0,
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Font = OsuFont.Default.With(size: 24),
|
|
||||||
Y = -120,
|
|
||||||
},
|
|
||||||
new ArgonSpinnerDisc
|
new ArgonSpinnerDisc
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -85,19 +77,33 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBindable<double> gainedBonus = null!;
|
private IBindable<int> completedSpins = null!;
|
||||||
private IBindable<double> spinsPerMinute = null!;
|
private IBindable<double> spinsPerMinute = null!;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
|
completedSpins = drawableSpinner.CompletedFullSpins.GetBoundCopy();
|
||||||
gainedBonus.BindValueChanged(bonus =>
|
completedSpins.BindValueChanged(_ =>
|
||||||
{
|
{
|
||||||
bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
|
if (drawableSpinner.CurrentBonusScore <= 0)
|
||||||
bonusCounter.FadeOutFromOne(1500);
|
return;
|
||||||
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
|
||||||
|
if (drawableSpinner.CurrentBonusScore == drawableSpinner.MaximumBonusScore)
|
||||||
|
{
|
||||||
|
bonusCounter.Text = "MAX";
|
||||||
|
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(2.8f, 1000, Easing.OutQuint);
|
||||||
|
|
||||||
|
bonusCounter.FlashColour(Colour4.FromHex("FC618F"), 400);
|
||||||
|
bonusCounter.FadeOutFromOne(500);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bonusCounter.Text = drawableSpinner.CurrentBonusScore.ToString(NumberFormatInfo.InvariantInfo);
|
||||||
|
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||||
|
bonusCounter.FadeOutFromOne(1500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
|
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||||
|
@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
private Container spmContainer = null!;
|
private Container spmContainer = null!;
|
||||||
private OsuSpriteText spmCounter = null!;
|
private OsuSpriteText spmCounter = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
public DefaultSpinner()
|
public DefaultSpinner()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -80,19 +83,33 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBindable<double> gainedBonus = null!;
|
private IBindable<int> completedSpins = null!;
|
||||||
private IBindable<double> spinsPerMinute = null!;
|
private IBindable<double> spinsPerMinute = null!;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
|
completedSpins = drawableSpinner.CompletedFullSpins.GetBoundCopy();
|
||||||
gainedBonus.BindValueChanged(bonus =>
|
completedSpins.BindValueChanged(bonus =>
|
||||||
{
|
{
|
||||||
bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
|
if (drawableSpinner.CurrentBonusScore <= 0)
|
||||||
bonusCounter.FadeOutFromOne(1500);
|
return;
|
||||||
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
|
||||||
|
if (drawableSpinner.CurrentBonusScore == drawableSpinner.MaximumBonusScore)
|
||||||
|
{
|
||||||
|
bonusCounter.Text = "MAX";
|
||||||
|
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(2.8f, 1000, Easing.OutQuint);
|
||||||
|
|
||||||
|
bonusCounter.FlashColour(colours.YellowLight, 400);
|
||||||
|
bonusCounter.FadeOutFromOne(500);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bonusCounter.Text = drawableSpinner.CurrentBonusScore.ToString(NumberFormatInfo.InvariantInfo);
|
||||||
|
bonusCounter.FadeOutFromOne(1500);
|
||||||
|
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
|
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||||
|
@ -101,15 +101,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
rotationTransferred = true;
|
rotationTransferred = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentRotation += delta;
|
|
||||||
|
|
||||||
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
|
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
|
||||||
|
delta = (float)(delta * Math.Abs(rate));
|
||||||
|
|
||||||
Debug.Assert(Math.Abs(delta) <= 180);
|
Debug.Assert(Math.Abs(delta) <= 180);
|
||||||
|
|
||||||
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
|
currentRotation += delta;
|
||||||
// (see: ModTimeRamp)
|
drawableSpinner.Result.History.ReportDelta(Time.Current, delta);
|
||||||
drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetState(DrawableHitObject obj)
|
private void resetState(DrawableHitObject obj)
|
||||||
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBindable<double> gainedBonus = null!;
|
private IBindable<int> completedSpins = null!;
|
||||||
private IBindable<double> spinsPerMinute = null!;
|
private IBindable<double> spinsPerMinute = null!;
|
||||||
|
|
||||||
private readonly Bindable<bool> completed = new Bindable<bool>();
|
private readonly Bindable<bool> completed = new Bindable<bool>();
|
||||||
@ -116,12 +116,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
|
completedSpins = DrawableSpinner.CompletedFullSpins.GetBoundCopy();
|
||||||
gainedBonus.BindValueChanged(bonus =>
|
completedSpins.BindValueChanged(bonus =>
|
||||||
{
|
{
|
||||||
bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
|
if (DrawableSpinner.CurrentBonusScore <= 0)
|
||||||
bonusCounter.FadeOutFromOne(800, Easing.Out);
|
return;
|
||||||
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
|
|
||||||
|
bonusCounter.Text = DrawableSpinner.CurrentBonusScore.ToString(NumberFormatInfo.InvariantInfo);
|
||||||
|
|
||||||
|
if (DrawableSpinner.CurrentBonusScore == DrawableSpinner.MaximumBonusScore)
|
||||||
|
{
|
||||||
|
bonusCounter.ScaleTo(1.4f).Then().ScaleTo(1.8f, 1000, Easing.Out);
|
||||||
|
bonusCounter.FadeOutFromOne(500, Easing.Out);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bonusCounter.FadeOutFromOne(800, Easing.Out);
|
||||||
|
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
|
spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -208,8 +209,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
|||||||
if (score.HitEvents.Count == 0)
|
if (score.HitEvents.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Todo: This should probably not be done like this.
|
float radius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(playableBeatmap.Difficulty.CircleSize, true);
|
||||||
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2;
|
|
||||||
|
|
||||||
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
|
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
|
||||||
{
|
{
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Filter;
|
using osu.Game.Rulesets.Filter;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Carousel;
|
||||||
using osu.Game.Screens.Select.Filter;
|
using osu.Game.Screens.Select.Filter;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual.Filtering
|
namespace osu.Game.Tests.NonVisual.Filtering
|
||||||
@ -382,6 +384,57 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
|
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("[1]", new[] { 0 })]
|
||||||
|
[TestCase("[1", new[] { 0 })]
|
||||||
|
[TestCase("My[Favourite", new[] { 2 })]
|
||||||
|
[TestCase("My[Favourite]", new[] { 2 })]
|
||||||
|
[TestCase("My[Favourite]Song", new[] { 2 })]
|
||||||
|
[TestCase("Favourite]", new[] { 2 })]
|
||||||
|
[TestCase("[Diff", new[] { 0, 1, 3, 4, 6 })]
|
||||||
|
[TestCase("[Diff]", new[] { 0, 1, 3, 4, 6 })]
|
||||||
|
[TestCase("[Favourite]", new[] { 3 })]
|
||||||
|
[TestCase("Title1 [Diff]", new[] { 0, 1 })]
|
||||||
|
[TestCase("Title1[Diff]", new int[] { })]
|
||||||
|
[TestCase("[diff ]with]", new[] { 4 })]
|
||||||
|
[TestCase("[diff ]with [[ brackets]]]]", new[] { 4 })]
|
||||||
|
[TestCase("[Diff in title]", new int[] { })]
|
||||||
|
[TestCase("[Diff in diff]", new[] { 6 })]
|
||||||
|
[TestCase("diff=Diff", new[] { 0, 1, 3, 4, 6 })]
|
||||||
|
[TestCase("diff=Diff1", new[] { 0 })]
|
||||||
|
[TestCase("diff=\"Diff\"", new[] { 3, 4, 6 })]
|
||||||
|
[TestCase("diff=!\"Diff\"", new int[] { })]
|
||||||
|
public void TestDifficultySearch(string query, int[] expectedBeatmapIndexes)
|
||||||
|
{
|
||||||
|
var carouselBeatmaps = (((string title, string difficultyName)[])new[]
|
||||||
|
{
|
||||||
|
("Title1", "Diff1"),
|
||||||
|
("Title1", "Diff2"),
|
||||||
|
("My[Favourite]Song", "Expert"),
|
||||||
|
("Title", "My Favourite Diff"),
|
||||||
|
("Another One", "diff ]with [[ brackets]]]"),
|
||||||
|
("Diff in title", "a"),
|
||||||
|
("a", "Diff in diff"),
|
||||||
|
}).Select(info => new CarouselBeatmap(new BeatmapInfo
|
||||||
|
{
|
||||||
|
Metadata = new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Title = info.title
|
||||||
|
},
|
||||||
|
DifficultyName = info.difficultyName
|
||||||
|
})).ToList();
|
||||||
|
|
||||||
|
var criteria = new FilterCriteria();
|
||||||
|
|
||||||
|
FilterQueryParser.ApplyQueries(criteria, query);
|
||||||
|
carouselBeatmaps.ForEach(b => b.Filter(criteria));
|
||||||
|
|
||||||
|
int[] visibleBeatmaps = carouselBeatmaps
|
||||||
|
.Where(b => !b.Filtered.Value)
|
||||||
|
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();
|
||||||
|
|
||||||
|
Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
|
||||||
|
}
|
||||||
|
|
||||||
private class CustomFilterCriteria : IRulesetFilterCriteria
|
private class CustomFilterCriteria : IRulesetFilterCriteria
|
||||||
{
|
{
|
||||||
public string? CustomValue { get; set; }
|
public string? CustomValue { get; set; }
|
||||||
|
@ -64,7 +64,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
new[] { "Plain", "This is plain comment" },
|
new[] { "Plain", "This is plain comment" },
|
||||||
new[] { "Pinned", "This is pinned comment" },
|
new[] { "Pinned", "This is pinned comment" },
|
||||||
new[] { "Link", "Please visit https://osu.ppy.sh" },
|
new[] { "Link", "Please visit https://osu.ppy.sh" },
|
||||||
|
new[] { "Big Image", "![](Backgrounds/bg1)" },
|
||||||
|
new[] { "Small Image", "![](Cursor/cursortrail)" },
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"Heading", @"# Heading 1
|
"Heading", @"# Heading 1
|
||||||
|
@ -55,13 +55,20 @@ namespace osu.Game.Beatmaps
|
|||||||
static double DifficultyRange(double difficulty, double min, double mid, double max)
|
static double DifficultyRange(double difficulty, double min, double mid, double max)
|
||||||
{
|
{
|
||||||
if (difficulty > 5)
|
if (difficulty > 5)
|
||||||
return mid + (max - mid) * (difficulty - 5) / 5;
|
return mid + (max - mid) * DifficultyRange(difficulty);
|
||||||
if (difficulty < 5)
|
if (difficulty < 5)
|
||||||
return mid - (mid - min) * (5 - difficulty) / 5;
|
return mid + (mid - min) * DifficultyRange(difficulty);
|
||||||
|
|
||||||
return mid;
|
return mid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a difficulty value [0, 10] to a linear range of [-1, 1].
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="difficulty">The difficulty value to be mapped.</param>
|
||||||
|
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
|
||||||
|
static double DifficultyRange(double difficulty) => (difficulty - 5) / 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
|
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -2,8 +2,14 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using Markdig.Syntax;
|
using Markdig.Syntax;
|
||||||
|
using Markdig.Syntax.Inlines;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Containers.Markdown;
|
using osu.Framework.Graphics.Containers.Markdown;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Graphics.Containers.Markdown;
|
using osu.Game.Graphics.Containers.Markdown;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Comments
|
namespace osu.Game.Overlays.Comments
|
||||||
{
|
{
|
||||||
@ -16,6 +22,8 @@ namespace osu.Game.Overlays.Comments
|
|||||||
|
|
||||||
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
|
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
|
||||||
|
|
||||||
|
public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
|
||||||
|
|
||||||
private partial class CommentMarkdownHeading : OsuMarkdownHeading
|
private partial class CommentMarkdownHeading : OsuMarkdownHeading
|
||||||
{
|
{
|
||||||
public CommentMarkdownHeading(HeadingBlock headingBlock)
|
public CommentMarkdownHeading(HeadingBlock headingBlock)
|
||||||
@ -40,5 +48,64 @@ namespace osu.Game.Overlays.Comments
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class CommentMarkdownTextFlowContainer : MarkdownTextFlowContainer
|
||||||
|
{
|
||||||
|
protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline.Url));
|
||||||
|
|
||||||
|
private partial class CommentMarkdownImage : MarkdownImage
|
||||||
|
{
|
||||||
|
public CommentMarkdownImage(string url)
|
||||||
|
: base(url)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private DelayedLoadWrapper wrapper = null!;
|
||||||
|
|
||||||
|
protected override Drawable CreateContent(string url) => wrapper = new DelayedLoadWrapper(CreateImageContainer(url));
|
||||||
|
|
||||||
|
protected override ImageContainer CreateImageContainer(string url)
|
||||||
|
{
|
||||||
|
var container = new CommentImageContainer(url);
|
||||||
|
container.OnLoadComplete += d =>
|
||||||
|
{
|
||||||
|
// The size of DelayedLoadWrapper depends on AutoSizeAxes of it's content.
|
||||||
|
// But since it's set to None, we need to specify the size here manually.
|
||||||
|
wrapper.Size = container.Size;
|
||||||
|
d.FadeInFromZero(300, Easing.OutQuint);
|
||||||
|
};
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class CommentImageContainer : ImageContainer
|
||||||
|
{
|
||||||
|
// https://github.com/ppy/osu-web/blob/3bd0f406dc78d60b356d955cd4201f8c3e1cca09/resources/css/bem/osu-md.less#L36
|
||||||
|
// Web version defines max height in em units (6 em), which assuming default font size as 14 results in 84 px,
|
||||||
|
// which also seems to match observations upon inspecting the web element.
|
||||||
|
private const float max_height = 84f;
|
||||||
|
|
||||||
|
public CommentImageContainer(string url)
|
||||||
|
: base(url)
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Sprite CreateImageSprite() => new Sprite
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override Texture GetImageTexture(TextureStore textures, string url)
|
||||||
|
{
|
||||||
|
Texture t = base.GetImageTexture(textures, url);
|
||||||
|
|
||||||
|
if (t != null)
|
||||||
|
Size = t.Height > max_height ? new Vector2(max_height / t.Height * t.Width, max_height) : t.Size;
|
||||||
|
|
||||||
|
return t!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,26 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool IsInitialized;
|
internal bool IsInitialized;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum allowable volume for sample playback.
|
||||||
|
/// <see cref="Samples"/> quieter than that will be forcibly played at this volume instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Drawable hitobjects adding their own custom samples, or other sample playback sources
|
||||||
|
/// (i.e. <see cref="GameplaySampleTriggerSource"/>) must enforce this themselves.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This sample volume floor is present in stable, although it is set at 8% rather than 5%.
|
||||||
|
/// See: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1070,
|
||||||
|
/// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Audio/AudioEngine.cs#L1404-L1405.
|
||||||
|
/// The reason why it is 5% here is that the 8% cap was enforced in a silent manner
|
||||||
|
/// (i.e. the minimum selectable volume in the editor was 5%, but it would be played at 8% anyways),
|
||||||
|
/// which is confusing and arbitrary, so we're just doing 5% here at the cost of sacrificing strict parity.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public const int MINIMUM_SAMPLE_VOLUME = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="DrawableHitObject"/>.
|
/// Creates a new <see cref="DrawableHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -181,7 +201,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount);
|
comboColourBrightness.BindTo(gameplaySettings.ComboColourNormalisationAmount);
|
||||||
|
|
||||||
// Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal.
|
// Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal.
|
||||||
base.AddInternal(Samples = new PausableSkinnableSound());
|
base.AddInternal(Samples = new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME
|
||||||
|
});
|
||||||
|
|
||||||
CurrentSkin = skinSource;
|
CurrentSkin = skinSource;
|
||||||
CurrentSkin.SourceChanged += skinSourceChanged;
|
CurrentSkin.SourceChanged += skinSourceChanged;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
|
||||||
@ -38,5 +39,23 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
|
|
||||||
return timingControlPoint.BeatLength * bpmMultiplier;
|
return timingControlPoint.BeatLength * bpmMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates scale from a CS value, with an optional fudge that was historically applied to the osu! ruleset.
|
||||||
|
/// </summary>
|
||||||
|
public static float CalculateScaleFromCircleSize(float circleSize, bool applyFudge = false)
|
||||||
|
{
|
||||||
|
// The following comment is copied verbatim from osu-stable:
|
||||||
|
//
|
||||||
|
// Builds of osu! up to 2013-05-04 had the gamefield being rounded down, which caused incorrect radius calculations
|
||||||
|
// in widescreen cases. This ratio adjusts to allow for old replays to work post-fix, which in turn increases the lenience
|
||||||
|
// for all plays, but by an amount so small it should only be effective in replays.
|
||||||
|
//
|
||||||
|
// To match expectations of gameplay we need to apply this multiplier to circle scale. It's weird but is what it is.
|
||||||
|
// It works out to under 1 game pixel and is generally not meaningful to gameplay, but is to replay playback accuracy.
|
||||||
|
const float broken_gamefield_rounding_allowance = 1.00041f;
|
||||||
|
|
||||||
|
return (float)(1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -45,7 +46,10 @@ namespace osu.Game.Rulesets.UI
|
|||||||
Child = hitSounds = new Container<SkinnableSound>
|
Child = hitSounds = new Container<SkinnableSound>
|
||||||
{
|
{
|
||||||
Name = "concurrent sample pool",
|
Name = "concurrent sample pool",
|
||||||
ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound())
|
ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound
|
||||||
|
{
|
||||||
|
MinimumSampleVolume = DrawableHitObject.MINIMUM_SAMPLE_VOLUME
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ using osu.Game.Audio;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Screens.Edit.Timing;
|
using osu.Game.Screens.Edit.Timing;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
},
|
},
|
||||||
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
|
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
|
||||||
{
|
{
|
||||||
MinValue = 0,
|
MinValue = DrawableHitObject.MINIMUM_SAMPLE_VOLUME,
|
||||||
MaxValue = 100,
|
MaxValue = 100,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1197,6 +1197,8 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
private bool rightMouseScrollBlocked;
|
private bool rightMouseScrollBlocked;
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||||
|
|
||||||
public CarouselScrollContainer()
|
public CarouselScrollContainer()
|
||||||
{
|
{
|
||||||
// size is determined by the carousel itself, due to not all content necessarily being loaded.
|
// size is determined by the carousel itself, due to not all content necessarily being loaded.
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
||||||
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
|
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
|
||||||
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
|
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
|
||||||
|
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName);
|
||||||
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
||||||
|
|
||||||
if (!match) return false;
|
if (!match) return false;
|
||||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Screens.Select
|
|||||||
public OptionalTextFilter Creator;
|
public OptionalTextFilter Creator;
|
||||||
public OptionalTextFilter Artist;
|
public OptionalTextFilter Artist;
|
||||||
public OptionalTextFilter Title;
|
public OptionalTextFilter Title;
|
||||||
|
public OptionalTextFilter DifficultyName;
|
||||||
|
|
||||||
public OptionalRange<double> UserStarDifficulty = new OptionalRange<double>
|
public OptionalRange<double> UserStarDifficulty = new OptionalRange<double>
|
||||||
{
|
{
|
||||||
@ -68,8 +69,23 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
string remainingText = value;
|
string remainingText = value;
|
||||||
|
|
||||||
|
// Match either an open difficulty tag to the end of string,
|
||||||
|
// or match a closed one with a whitespace after it.
|
||||||
|
//
|
||||||
|
// To keep things simple, the closing ']' may be included in the match group,
|
||||||
|
// and is trimmed post-match.
|
||||||
|
foreach (Match quotedSegment in Regex.Matches(value, "(^|\\s)\\[(.*)(\\]\\s|$)"))
|
||||||
|
{
|
||||||
|
DifficultyName = new OptionalTextFilter
|
||||||
|
{
|
||||||
|
SearchTerm = quotedSegment.Groups[2].Value.Trim(']')
|
||||||
|
};
|
||||||
|
|
||||||
|
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
// First handle quoted segments to ensure we keep inline spaces in exact matches.
|
// First handle quoted segments to ensure we keep inline spaces in exact matches.
|
||||||
foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)"))
|
foreach (Match quotedSegment in Regex.Matches(value, "(\"[^\"]+\"[!]?)"))
|
||||||
{
|
{
|
||||||
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
|
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
|
||||||
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
|
||||||
|
@ -76,6 +76,9 @@ namespace osu.Game.Screens.Select
|
|||||||
case "title":
|
case "title":
|
||||||
return TryUpdateCriteriaText(ref criteria.Title, op, value);
|
return TryUpdateCriteriaText(ref criteria.Title, op, value);
|
||||||
|
|
||||||
|
case "diff":
|
||||||
|
return TryUpdateCriteriaText(ref criteria.DifficultyName, op, value);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
||||||
}
|
}
|
||||||
|
@ -171,11 +171,6 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
new ResetScrollContainer(() => Carousel.ScrollToSelected())
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
Width = 250,
|
|
||||||
},
|
|
||||||
new VerticalMaskingContainer
|
new VerticalMaskingContainer
|
||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
@ -243,6 +238,10 @@ namespace osu.Game.Screens.Select
|
|||||||
Padding = new MarginPadding { Top = left_area_padding },
|
Padding = new MarginPadding { Top = left_area_padding },
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
new LeftSideInteractionContainer(() => Carousel.ScrollToSelected())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
beatmapInfoWedge = new BeatmapInfoWedge
|
beatmapInfoWedge = new BeatmapInfoWedge
|
||||||
{
|
{
|
||||||
Height = WEDGE_HEIGHT,
|
Height = WEDGE_HEIGHT,
|
||||||
@ -1017,18 +1016,25 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class ResetScrollContainer : Container
|
/// <summary>
|
||||||
|
/// Handles mouse interactions required when moving away from the carousel.
|
||||||
|
/// </summary>
|
||||||
|
private partial class LeftSideInteractionContainer : Container
|
||||||
{
|
{
|
||||||
private readonly Action? onHoverAction;
|
private readonly Action? resetCarouselPosition;
|
||||||
|
|
||||||
public ResetScrollContainer(Action onHoverAction)
|
public LeftSideInteractionContainer(Action resetCarouselPosition)
|
||||||
{
|
{
|
||||||
this.onHoverAction = onHoverAction;
|
this.resetCarouselPosition = resetCarouselPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool OnScroll(ScrollEvent e) => true;
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
onHoverAction?.Invoke();
|
resetCarouselPosition?.Invoke();
|
||||||
return base.OnHover(e);
|
return base.OnHover(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,12 @@ namespace osu.Game.Skinning
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent
|
public partial class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum allowable volume for <see cref="Samples"/>.
|
||||||
|
/// <see cref="Samples"/> that specify a lower <see cref="ISampleInfo.Volume"/> will be forcibly pulled up to this volume.
|
||||||
|
/// </summary>
|
||||||
|
public int MinimumSampleVolume { get; set; }
|
||||||
|
|
||||||
public override bool RemoveWhenNotAlive => false;
|
public override bool RemoveWhenNotAlive => false;
|
||||||
public override bool RemoveCompletedTransforms => false;
|
public override bool RemoveCompletedTransforms => false;
|
||||||
|
|
||||||
@ -156,7 +162,7 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s);
|
var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s);
|
||||||
sample.Looping = Looping;
|
sample.Looping = Looping;
|
||||||
sample.Volume.Value = s.Volume / 100.0;
|
sample.Volume.Value = Math.Max(s.Volume, MinimumSampleVolume) / 100.0;
|
||||||
|
|
||||||
samplesContainer.Add(sample);
|
samplesContainer.Add(sample);
|
||||||
}
|
}
|
||||||
|
@ -432,6 +432,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
private bool running;
|
private bool running;
|
||||||
|
|
||||||
|
public override double Rate => base.Rate
|
||||||
|
// This is mainly to allow some tests to override the rate to zero
|
||||||
|
// and avoid interpolation.
|
||||||
|
* referenceClock.Rate;
|
||||||
|
|
||||||
public TrackVirtualManual(IFrameBasedClock referenceClock, string name = "virtual")
|
public TrackVirtualManual(IFrameBasedClock referenceClock, string name = "virtual")
|
||||||
: base(name)
|
: base(name)
|
||||||
{
|
{
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="11.5.0" />
|
<PackageReference Include="Realm" Version="11.5.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1012.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2023.1012.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1014.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1023.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.40.0" />
|
<PackageReference Include="Sentry" Version="3.40.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.34.1" />
|
<PackageReference Include="SharpCompress" Version="0.34.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
|
Loading…
Reference in New Issue
Block a user