mirror of
https://github.com/ppy/osu.git
synced 2026-05-20 01:59:54 +08:00
Compare commits
1265 Commits
@@ -27,10 +27,10 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2021.705.0",
|
||||
"version": "2021.725.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,3 +190,5 @@ dotnet_diagnostic.CA2225.severity = none
|
||||
|
||||
# Banned APIs
|
||||
dotnet_diagnostic.RS0030.severity = error
|
||||
|
||||
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# osu!
|
||||
|
||||
[](https://ci.appveyor.com/project/peppy/osu)
|
||||
[](https://github.com/ppy/osu/actions/workflows/ci.yml)
|
||||
[](https://github.com/ppy/osu/releases/latest)
|
||||
[](https://www.codefactor.io/repository/github/ppy/osu)
|
||||
[](https://discord.gg/ppy)
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.722.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.723.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.828.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
||||
@@ -20,8 +20,21 @@ namespace osu.Android
|
||||
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
||||
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
|
||||
{
|
||||
"application/zip",
|
||||
"application/octet-stream",
|
||||
"application/download",
|
||||
"application/x-zip",
|
||||
"application/x-zip-compressed",
|
||||
// newer official mime types (see https://osu.ppy.sh/wiki/en/osu%21_File_Formats).
|
||||
"application/x-osu-beatmap-archive",
|
||||
"application/x-osu-skin-archive",
|
||||
"application/x-osu-replay",
|
||||
})]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
|
||||
public class OsuGameActivity : AndroidGameActivity
|
||||
{
|
||||
@@ -66,12 +79,14 @@ namespace osu.Android
|
||||
case Intent.ActionSendMultiple:
|
||||
{
|
||||
var uris = new List<Uri>();
|
||||
|
||||
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
|
||||
{
|
||||
var content = intent.ClipData?.GetItemAt(i);
|
||||
if (content != null)
|
||||
uris.Add(content.Uri);
|
||||
}
|
||||
|
||||
handleImportFromUris(uris.ToArray());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -139,8 +139,8 @@ namespace osu.Desktop
|
||||
{
|
||||
switch (activity)
|
||||
{
|
||||
case UserActivity.SoloGame solo:
|
||||
return solo.Beatmap.ToString();
|
||||
case UserActivity.InGame game:
|
||||
return game.Beatmap.ToString();
|
||||
|
||||
case UserActivity.Editing edit:
|
||||
return edit.Beatmap.ToString();
|
||||
|
||||
@@ -5,23 +5,23 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
public class GameplayWinKeyBlocker : Component
|
||||
{
|
||||
private Bindable<bool> disableWinKey;
|
||||
private Bindable<bool> localUserPlaying;
|
||||
private IBindable<bool> localUserPlaying;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGame game, OsuConfigManager config)
|
||||
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
|
||||
{
|
||||
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
|
||||
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
|
||||
localUserPlaying.BindValueChanged(_ => updateBlocking());
|
||||
|
||||
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 BenchmarkDotNet.Attributes;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkMod : BenchmarkTest
|
||||
{
|
||||
private OsuModDoubleTime mod;
|
||||
|
||||
[Params(1, 10, 100)]
|
||||
public int Times { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
mod = new OsuModDoubleTime();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int ModHashCode()
|
||||
{
|
||||
var hashCode = new HashCode();
|
||||
|
||||
for (int i = 0; i < Times; i++)
|
||||
hashCode.Add(mod);
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,9 +210,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
new Vector2(50, 200),
|
||||
}), 0.5);
|
||||
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
|
||||
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.PerfectCurve);
|
||||
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve);
|
||||
addAddVertexSteps(150, 150);
|
||||
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.Linear);
|
||||
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear);
|
||||
}
|
||||
|
||||
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
} while (rng.Next(2) != 0);
|
||||
|
||||
int length = sliderPath.ControlPoints.Count - start + 1;
|
||||
sliderPath.ControlPoints[start].Type.Value = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
|
||||
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
|
||||
} while (rng.Next(3) != 0);
|
||||
|
||||
if (rng.Next(5) == 0)
|
||||
@@ -210,13 +210,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
path.ConvertToSliderPath(sliderPath, sliderStartY);
|
||||
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
|
||||
Assert.That(sliderPath.ControlPoints[0].Position.Value.X, Is.EqualTo(path.Vertices[0].X));
|
||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
||||
assertInvariants(path.Vertices, true);
|
||||
|
||||
foreach (var point in sliderPath.ControlPoints)
|
||||
{
|
||||
Assert.That(point.Type.Value, Is.EqualTo(PathType.Linear).Or.Null);
|
||||
Assert.That(sliderStartY + point.Position.Value.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||
Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null);
|
||||
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchModMirrorTest
|
||||
{
|
||||
[Test]
|
||||
public void TestModMirror()
|
||||
{
|
||||
IBeatmap original = createBeatmap(false);
|
||||
IBeatmap mirrored = createBeatmap(true);
|
||||
|
||||
assertEffectivePositionsMirrored(original, mirrored);
|
||||
}
|
||||
|
||||
private static IBeatmap createBeatmap(bool withMirrorMod)
|
||||
{
|
||||
var beatmap = createRawBeatmap();
|
||||
var mirrorMod = new CatchModMirror();
|
||||
|
||||
var beatmapProcessor = new CatchBeatmapProcessor(beatmap);
|
||||
beatmapProcessor.PreProcess();
|
||||
|
||||
foreach (var hitObject in beatmap.HitObjects)
|
||||
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
beatmapProcessor.PostProcess();
|
||||
|
||||
if (withMirrorMod)
|
||||
mirrorMod.ApplyToBeatmap(beatmap);
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
private static IBeatmap createRawBeatmap() => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Fruit
|
||||
{
|
||||
OriginalX = 150,
|
||||
StartTime = 0
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
OriginalX = 450,
|
||||
StartTime = 500
|
||||
},
|
||||
new JuiceStream
|
||||
{
|
||||
OriginalX = 250,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(new Vector2(-100, 1)),
|
||||
new PathControlPoint(new Vector2(0, 2)),
|
||||
new PathControlPoint(new Vector2(100, 3)),
|
||||
new PathControlPoint(new Vector2(0, 4))
|
||||
}
|
||||
},
|
||||
StartTime = 1000,
|
||||
},
|
||||
new BananaShower
|
||||
{
|
||||
StartTime = 5000,
|
||||
Duration = 5000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static void assertEffectivePositionsMirrored(IBeatmap original, IBeatmap mirrored)
|
||||
{
|
||||
if (original.HitObjects.Count != mirrored.HitObjects.Count)
|
||||
Assert.Fail($"Top-level object count mismatch (original: {original.HitObjects.Count}, mirrored: {mirrored.HitObjects.Count})");
|
||||
|
||||
for (int i = 0; i < original.HitObjects.Count; ++i)
|
||||
{
|
||||
var originalObject = (CatchHitObject)original.HitObjects[i];
|
||||
var mirroredObject = (CatchHitObject)mirrored.HitObjects[i];
|
||||
|
||||
// banana showers themselves are exempt, as we only really care about their nested bananas' positions.
|
||||
if (!effectivePositionMirrored(originalObject, mirroredObject) && !(originalObject is BananaShower))
|
||||
Assert.Fail($"{originalObject.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalObject, mirroredObject)})");
|
||||
|
||||
if (originalObject.NestedHitObjects.Count != mirroredObject.NestedHitObjects.Count)
|
||||
Assert.Fail($"{originalObject.GetType().Name} nested object count mismatch (original: {originalObject.NestedHitObjects.Count}, mirrored: {mirroredObject.NestedHitObjects.Count})");
|
||||
|
||||
for (int j = 0; j < originalObject.NestedHitObjects.Count; ++j)
|
||||
{
|
||||
var originalNested = (CatchHitObject)originalObject.NestedHitObjects[j];
|
||||
var mirroredNested = (CatchHitObject)mirroredObject.NestedHitObjects[j];
|
||||
|
||||
if (!effectivePositionMirrored(originalNested, mirroredNested))
|
||||
Assert.Fail($"{originalObject.GetType().Name}'s nested {originalNested.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalNested, mirroredNested)})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string printEffectivePositions(CatchHitObject original, CatchHitObject mirrored)
|
||||
=> $"original X: {original.EffectiveX}, mirrored X is: {mirrored.EffectiveX}, mirrored X should be: {CatchPlayfield.WIDTH - original.EffectiveX}";
|
||||
|
||||
private static bool effectivePositionMirrored(CatchHitObject original, CatchHitObject mirrored)
|
||||
=> Precision.AlmostEquals(original.EffectiveX, CatchPlayfield.WIDTH - mirrored.EffectiveX);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
var skin = new TestSkin { FlipCatcherPlate = flip };
|
||||
container.Child = new SkinProvidingContainer(skin)
|
||||
{
|
||||
Child = catcher = new Catcher(new Container(), new DroppedObjectContainer())
|
||||
Child = catcher = new Catcher(new DroppedObjectContainer())
|
||||
{
|
||||
Anchor = Anchor.Centre
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
private Container trailContainer;
|
||||
|
||||
private DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
private TestCatcher catcher;
|
||||
@@ -45,7 +43,6 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
CircleSize = 0,
|
||||
};
|
||||
|
||||
trailContainer = new Container();
|
||||
droppedObjectContainer = new DroppedObjectContainer();
|
||||
|
||||
Child = new Container
|
||||
@@ -54,8 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Children = new Drawable[]
|
||||
{
|
||||
droppedObjectContainer,
|
||||
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty),
|
||||
trailContainer,
|
||||
catcher = new TestCatcher(droppedObjectContainer, difficulty),
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -294,8 +290,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
|
||||
|
||||
public TestCatcher(Container trailsTarget, DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
|
||||
: base(trailsTarget, droppedObjectTarget, difficulty)
|
||||
public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
|
||||
: base(droppedObjectTarget, difficulty)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
|
||||
AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
|
||||
AddToggleStep("toggle hit lighting", lighting => config.SetValue(OsuSetting.HitLighting, lighting));
|
||||
|
||||
AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
|
||||
AddStep("catch many random fruit", () =>
|
||||
@@ -122,10 +123,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
|
||||
{
|
||||
var droppedObjectContainer = new DroppedObjectContainer();
|
||||
|
||||
Add(droppedObjectContainer);
|
||||
|
||||
Catcher = new Catcher(this, droppedObjectContainer, beatmapDifficulty)
|
||||
Catcher = new Catcher(droppedObjectContainer, beatmapDifficulty)
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomEndGlowColour()
|
||||
public void TestCustomAfterImageColour()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomEndGlowColourPriority()
|
||||
public void TestCustomAfterImageColourPriority()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
@@ -111,39 +111,37 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
||||
}
|
||||
|
||||
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
|
||||
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedAfterImageColour = null)
|
||||
{
|
||||
Container trailsContainer = null;
|
||||
Catcher catcher = null;
|
||||
CatcherTrailDisplay trails = null;
|
||||
Catcher catcher = null;
|
||||
|
||||
AddStep("create hyper-dashing catcher", () =>
|
||||
{
|
||||
trailsContainer = new Container();
|
||||
CatcherArea catcherArea;
|
||||
Child = setupSkinHierarchy(new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
Child = catcherArea = new CatcherArea
|
||||
{
|
||||
catcher = new Catcher(trailsContainer, new DroppedObjectContainer())
|
||||
Catcher = catcher = new Catcher(new DroppedObjectContainer())
|
||||
{
|
||||
Scale = new Vector2(4)
|
||||
},
|
||||
trailsContainer
|
||||
}
|
||||
}
|
||||
}, skin);
|
||||
trails = catcherArea.ChildrenOfType<CatcherTrailDisplay>().Single();
|
||||
});
|
||||
|
||||
AddStep("get trails container", () =>
|
||||
AddStep("start hyper-dash", () =>
|
||||
{
|
||||
trails = trailsContainer.OfType<CatcherTrailDisplay>().Single();
|
||||
catcher.SetHyperDashState(2);
|
||||
});
|
||||
|
||||
AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour);
|
||||
|
||||
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
|
||||
AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
|
||||
AddAssert("catcher after-image colours are correct", () => trails.HyperDashAfterImageColour == (expectedAfterImageColour ?? expectedCatcherColour));
|
||||
|
||||
AddStep("finish hyper-dashing", () =>
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
case JuiceStream juiceStream:
|
||||
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
|
||||
lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.Value.X;
|
||||
lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.X;
|
||||
|
||||
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
|
||||
lastStartTime = juiceStream.StartTime;
|
||||
|
||||
@@ -117,6 +117,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
new CatchModDifficultyAdjust(),
|
||||
new CatchModClassic(),
|
||||
new CatchModMirror(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
@@ -130,7 +131,8 @@ namespace osu.Game.Rulesets.Catch
|
||||
return new Mod[]
|
||||
{
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new CatchModFloatingFruits()
|
||||
new CatchModFloatingFruits(),
|
||||
new CatchModMuted(),
|
||||
};
|
||||
|
||||
default:
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
Banana,
|
||||
Droplet,
|
||||
Catcher,
|
||||
CatchComboCounter
|
||||
CatchComboCounter,
|
||||
HitExplosion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
{
|
||||
public class Movement : StrainSkill
|
||||
public class Movement : StrainDecaySkill
|
||||
{
|
||||
private const float absolute_player_positioning_error = 16f;
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
path.ConvertFromSliderPath(sliderPath);
|
||||
|
||||
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
||||
if (sliderPath.ControlPoints.Any(p => p.Type.Value != null && p.Type.Value != PathType.Linear))
|
||||
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
||||
{
|
||||
path.ResampleVertices(hitObject.NestedHitObjects
|
||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX);
|
||||
|
||||
foreach (var point in juiceStream.Path.ControlPoints)
|
||||
point.Position.Value *= new Vector2(-1, 1);
|
||||
point.Position *= new Vector2(-1, 1);
|
||||
|
||||
EditorBeatmap.Update(juiceStream);
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModMirror : ModMirror, IApplicableToBeatmap
|
||||
{
|
||||
public override string Description => "Fruits are flipped horizontally.";
|
||||
|
||||
/// <remarks>
|
||||
/// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>,
|
||||
/// as <see cref="CatchBeatmapProcessor"/> applies offsets in <see cref="CatchBeatmapProcessor.PostProcess"/>.
|
||||
/// <see cref="IApplicableToBeatmap"/> runs after post-processing, while <see cref="IApplicableToHitObject"/> runs before it.
|
||||
/// </remarks>
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
foreach (var hitObject in beatmap.HitObjects)
|
||||
applyToHitObject(hitObject);
|
||||
}
|
||||
|
||||
private void applyToHitObject(HitObject hitObject)
|
||||
{
|
||||
var catchObject = (CatchHitObject)hitObject;
|
||||
|
||||
switch (catchObject)
|
||||
{
|
||||
case Fruit fruit:
|
||||
mirrorEffectiveX(fruit);
|
||||
break;
|
||||
|
||||
case JuiceStream juiceStream:
|
||||
mirrorEffectiveX(juiceStream);
|
||||
mirrorJuiceStreamPath(juiceStream);
|
||||
break;
|
||||
|
||||
case BananaShower bananaShower:
|
||||
mirrorBananaShower(bananaShower);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the effective X position of <paramref name="catchObject"/> and its nested hit objects.
|
||||
/// </summary>
|
||||
private static void mirrorEffectiveX(CatchHitObject catchObject)
|
||||
{
|
||||
catchObject.OriginalX = CatchPlayfield.WIDTH - catchObject.OriginalX;
|
||||
catchObject.XOffset = -catchObject.XOffset;
|
||||
|
||||
foreach (var nested in catchObject.NestedHitObjects.Cast<CatchHitObject>())
|
||||
{
|
||||
nested.OriginalX = CatchPlayfield.WIDTH - nested.OriginalX;
|
||||
nested.XOffset = -nested.XOffset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the path of the <paramref name="juiceStream"/>.
|
||||
/// </summary>
|
||||
private static void mirrorJuiceStreamPath(JuiceStream juiceStream)
|
||||
{
|
||||
var controlPoints = juiceStream.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||
|
||||
juiceStream.Path = new SliderPath(controlPoints, juiceStream.Path.ExpectedDistance.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors X positions of all bananas in the <paramref name="bananaShower"/>.
|
||||
/// </summary>
|
||||
private static void mirrorBananaShower(BananaShower bananaShower)
|
||||
{
|
||||
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
|
||||
banana.XOffset = CatchPlayfield.WIDTH - banana.XOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModMuted : ModMuted<CatchHitObject>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value)));
|
||||
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
|
||||
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
for (int i = 1; i < vertices.Count; i++)
|
||||
{
|
||||
sliderPath.ControlPoints[^1].Type.Value = PathType.Linear;
|
||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||
|
||||
float deltaX = vertices[i].X - lastPosition.X;
|
||||
double length = vertices[i].Distance - currentDistance;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
{
|
||||
public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
|
||||
{
|
||||
private CircularContainer largeFaint;
|
||||
private CircularContainer smallFaint;
|
||||
private CircularContainer directionalGlow1;
|
||||
private CircularContainer directionalGlow2;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(20);
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
|
||||
// scale roughly in-line with visual appearance of notes
|
||||
const float initial_height = 10;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
largeFaint = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
smallFaint = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
directionalGlow1 = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Size = new Vector2(0.01f, initial_height),
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
directionalGlow2 = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Size = new Vector2(0.01f, initial_height),
|
||||
Blending = BlendingParameters.Additive,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void Animate(HitExplosionEntry entry)
|
||||
{
|
||||
X = entry.Position;
|
||||
Scale = new Vector2(entry.HitObject.Scale);
|
||||
setColour(entry.ObjectColour);
|
||||
|
||||
using (BeginAbsoluteSequence(entry.LifetimeStart))
|
||||
applyTransforms(entry.HitObject.RandomSeed);
|
||||
}
|
||||
|
||||
private void applyTransforms(int randomSeed)
|
||||
{
|
||||
const double duration = 400;
|
||||
|
||||
// we want our size to be very small so the glow dominates it.
|
||||
largeFaint.Size = new Vector2(0.8f);
|
||||
largeFaint
|
||||
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
|
||||
.FadeOut(duration * 2);
|
||||
|
||||
const float angle_variangle = 15; // should be less than 45
|
||||
directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
|
||||
directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
|
||||
|
||||
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
|
||||
}
|
||||
|
||||
private void setColour(Color4 objectColour)
|
||||
{
|
||||
const float roundness = 100;
|
||||
|
||||
largeFaint.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
|
||||
Roundness = 160,
|
||||
Radius = 200,
|
||||
};
|
||||
|
||||
smallFaint.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
|
||||
Roundness = 20,
|
||||
Radius = 50,
|
||||
};
|
||||
|
||||
directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
|
||||
Roundness = roundness,
|
||||
Radius = 40,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,13 +70,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
if (version < 2.3m)
|
||||
{
|
||||
if (GetTexture(@"fruit-ryuuta") != null ||
|
||||
GetTexture(@"fruit-ryuuta-0") != null)
|
||||
if (hasOldStyleCatcherSprite())
|
||||
return new LegacyCatcherOld();
|
||||
}
|
||||
|
||||
if (GetTexture(@"fruit-catcher-idle") != null ||
|
||||
GetTexture(@"fruit-catcher-idle-0") != null)
|
||||
if (hasNewStyleCatcherSprite())
|
||||
return new LegacyCatcherNew();
|
||||
|
||||
return null;
|
||||
@@ -86,12 +84,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return new LegacyCatchComboCounter(Skin);
|
||||
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.HitExplosion:
|
||||
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
|
||||
return new LegacyHitExplosion();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
private bool hasOldStyleCatcherSprite() =>
|
||||
GetTexture(@"fruit-ryuuta") != null
|
||||
|| GetTexture(@"fruit-ryuuta-0") != null;
|
||||
|
||||
private bool hasNewStyleCatcherSprite() =>
|
||||
GetTexture(@"fruit-catcher-idle") != null
|
||||
|| GetTexture(@"fruit-catcher-idle-0") != null;
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
|
||||
{
|
||||
[Resolved]
|
||||
private Catcher catcher { get; set; }
|
||||
|
||||
private const float catch_margin = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2;
|
||||
|
||||
private readonly Sprite explosion1;
|
||||
private readonly Sprite explosion2;
|
||||
|
||||
public LegacyHitExplosion()
|
||||
{
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Scale = new Vector2(0.5f);
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
explosion1 = new Sprite
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Rotation = -90
|
||||
},
|
||||
explosion2 = new Sprite
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Rotation = -90
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SkinManager skins)
|
||||
{
|
||||
var defaultLegacySkin = skins.DefaultLegacySkin;
|
||||
|
||||
// sprite names intentionally swapped to match stable member naming / ease of cross-referencing
|
||||
explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
|
||||
explosion2.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-1");
|
||||
}
|
||||
|
||||
public void Animate(HitExplosionEntry entry)
|
||||
{
|
||||
Colour = entry.ObjectColour;
|
||||
|
||||
using (BeginAbsoluteSequence(entry.LifetimeStart))
|
||||
{
|
||||
float halfCatchWidth = catcher.CatchWidth / 2;
|
||||
float explosionOffset = Math.Clamp(entry.Position, -halfCatchWidth + catch_margin * 3, halfCatchWidth - catch_margin * 3);
|
||||
|
||||
if (!(entry.HitObject is Droplet))
|
||||
{
|
||||
float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f);
|
||||
|
||||
explosion1.Scale = new Vector2(1, 0.9f);
|
||||
explosion1.Position = new Vector2(explosionOffset, 0);
|
||||
|
||||
explosion1.FadeOutFromOne(300);
|
||||
explosion1.ScaleTo(new Vector2(16 * scale, 1.1f), 160, Easing.Out);
|
||||
}
|
||||
|
||||
explosion2.Scale = new Vector2(0.9f, 1);
|
||||
explosion2.Position = new Vector2(explosionOffset, 0);
|
||||
|
||||
explosion2.FadeOutFromOne(700);
|
||||
explosion2.ScaleTo(new Vector2(0.9f, 1.3f), 500, Easing.Out);
|
||||
|
||||
this.Delay(700).FadeOutFromOne();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
@@ -45,14 +44,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var trailContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft
|
||||
};
|
||||
var droppedObjectContainer = new DroppedObjectContainer();
|
||||
|
||||
Catcher = new Catcher(trailContainer, droppedObjectContainer, difficulty)
|
||||
Catcher = new Catcher(droppedObjectContainer, difficulty)
|
||||
{
|
||||
X = CENTER_X
|
||||
};
|
||||
@@ -70,7 +64,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Origin = Anchor.TopLeft,
|
||||
Catcher = Catcher,
|
||||
},
|
||||
trailContainer,
|
||||
HitObjectContainer,
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
[Cached]
|
||||
public class Catcher : SkinReloadableDrawable
|
||||
{
|
||||
/// <summary>
|
||||
@@ -36,8 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public const float ALLOWED_CATCH_RANGE = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
|
||||
/// and end glow/after-image during a hyper-dash.
|
||||
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail and after-image during a hyper-dash.
|
||||
/// </summary>
|
||||
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
||||
|
||||
@@ -71,11 +71,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private const float caught_fruit_scale_adjust = 0.5f;
|
||||
|
||||
[NotNull]
|
||||
private readonly Container trailsTarget;
|
||||
|
||||
private CatcherTrailDisplay trails;
|
||||
|
||||
/// <summary>
|
||||
/// Contains caught objects on the plate.
|
||||
/// </summary>
|
||||
@@ -88,30 +83,22 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatcherAnimationState CurrentState
|
||||
{
|
||||
get => Body.AnimationState.Value;
|
||||
private set => Body.AnimationState.Value = value;
|
||||
get => body.AnimationState.Value;
|
||||
private set => body.AnimationState.Value = value;
|
||||
}
|
||||
|
||||
private bool dashing;
|
||||
|
||||
public bool Dashing
|
||||
{
|
||||
get => dashing;
|
||||
set
|
||||
{
|
||||
if (value == dashing) return;
|
||||
|
||||
dashing = value;
|
||||
|
||||
updateTrailVisibility();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Whether the catcher is currently dashing.
|
||||
/// </summary>
|
||||
public bool Dashing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently facing direction.
|
||||
/// </summary>
|
||||
public Direction VisualDirection { get; set; } = Direction.Right;
|
||||
|
||||
public Vector2 BodyScale => Scale * body.Scale;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
|
||||
/// </summary>
|
||||
@@ -120,12 +107,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <summary>
|
||||
/// Width of the area that can be used to attempt catches during gameplay.
|
||||
/// </summary>
|
||||
private readonly float catchWidth;
|
||||
public readonly float CatchWidth;
|
||||
|
||||
internal readonly SkinnableCatcher Body;
|
||||
private readonly SkinnableCatcher body;
|
||||
|
||||
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||
|
||||
private double hyperDashModifier = 1;
|
||||
private int hyperDashDirection;
|
||||
@@ -138,9 +124,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
|
||||
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
|
||||
|
||||
public Catcher([NotNull] Container trailsTarget, [NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
|
||||
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
this.trailsTarget = trailsTarget;
|
||||
this.droppedObjectTarget = droppedObjectTarget;
|
||||
|
||||
Origin = Anchor.TopCentre;
|
||||
@@ -149,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (difficulty != null)
|
||||
Scale = calculateScale(difficulty);
|
||||
|
||||
catchWidth = CalculateCatchWidth(Scale);
|
||||
CatchWidth = CalculateCatchWidth(Scale);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -164,7 +149,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
// offset fruit vertically to better place "above" the plate.
|
||||
Y = -5
|
||||
},
|
||||
Body = new SkinnableCatcher(),
|
||||
body = new SkinnableCatcher(),
|
||||
hitExplosionContainer = new HitExplosionContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -177,15 +162,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
|
||||
trails = new CatcherTrailDisplay(this);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// don't add in above load as we may potentially modify a parent in an unsafe manner.
|
||||
trailsTarget.Add(trails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,14 +194,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (!(hitObject is PalpableCatchHitObject fruit))
|
||||
return false;
|
||||
|
||||
var halfCatchWidth = catchWidth * 0.5f;
|
||||
|
||||
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
|
||||
var catchObjectPosition = fruit.EffectiveX;
|
||||
var catcherPosition = Position.X;
|
||||
|
||||
return catchObjectPosition >= catcherPosition - halfCatchWidth &&
|
||||
catchObjectPosition <= catcherPosition + halfCatchWidth;
|
||||
float halfCatchWidth = CatchWidth * 0.5f;
|
||||
return fruit.EffectiveX >= X - halfCatchWidth &&
|
||||
fruit.EffectiveX <= X + halfCatchWidth;
|
||||
}
|
||||
|
||||
public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result)
|
||||
@@ -246,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
placeCaughtObject(palpableObject, positionInStack);
|
||||
|
||||
if (hitLighting.Value)
|
||||
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
|
||||
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
|
||||
}
|
||||
|
||||
// droplet doesn't affect the catcher state
|
||||
@@ -312,10 +283,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
hyperDashTargetPosition = targetPosition;
|
||||
|
||||
if (!wasHyperDashing)
|
||||
{
|
||||
trails.DisplayEndGlow();
|
||||
runHyperDashStateTransition(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,13 +299,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
private void runHyperDashStateTransition(bool hyperDashing)
|
||||
{
|
||||
updateTrailVisibility();
|
||||
|
||||
this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
base.SkinChanged(skin);
|
||||
@@ -346,13 +310,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
||||
DEFAULT_HYPER_DASH_COLOUR;
|
||||
|
||||
hyperDashEndGlowColour =
|
||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ??
|
||||
hyperDashColour;
|
||||
|
||||
trails.HyperDashTrailsColour = hyperDashColour;
|
||||
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
|
||||
|
||||
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
|
||||
|
||||
runHyperDashStateTransition(HyperDashing);
|
||||
@@ -363,7 +320,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
base.Update();
|
||||
|
||||
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
|
||||
Body.Scale = scaleFromDirection;
|
||||
body.Scale = scaleFromDirection;
|
||||
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
|
||||
|
||||
// Correct overshooting.
|
||||
@@ -409,8 +366,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
return position;
|
||||
}
|
||||
|
||||
private void addLighting(CatchHitObject hitObject, float x, Color4 colour) =>
|
||||
hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed));
|
||||
private void addLighting(JudgementResult judgementResult, Color4 colour, float x) =>
|
||||
hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x));
|
||||
|
||||
private CaughtObject getCaughtObject(PalpableCatchHitObject source)
|
||||
{
|
||||
|
||||
@@ -25,17 +25,15 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public Catcher Catcher
|
||||
{
|
||||
get => catcher;
|
||||
set
|
||||
{
|
||||
if (catcher != null)
|
||||
Remove(catcher);
|
||||
|
||||
Add(catcher = value);
|
||||
}
|
||||
set => catcherContainer.Child = catcher = value;
|
||||
}
|
||||
|
||||
private readonly Container<Catcher> catcherContainer;
|
||||
|
||||
private readonly CatchComboDisplay comboDisplay;
|
||||
|
||||
private readonly CatcherTrailDisplay catcherTrails;
|
||||
|
||||
private Catcher catcher;
|
||||
|
||||
/// <summary>
|
||||
@@ -45,20 +43,28 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private int currentDirection;
|
||||
|
||||
// TODO: support replay rewind
|
||||
private bool lastHyperDashState;
|
||||
|
||||
/// <remarks>
|
||||
/// <see cref="Catcher"/> must be set before loading.
|
||||
/// </remarks>
|
||||
public CatcherArea()
|
||||
{
|
||||
Size = new Vector2(CatchPlayfield.WIDTH, Catcher.BASE_SIZE);
|
||||
Child = comboDisplay = new CatchComboDisplay
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Bottom = 350f },
|
||||
X = CatchPlayfield.CENTER_X
|
||||
catcherContainer = new Container<Catcher> { RelativeSizeAxes = Axes.Both },
|
||||
catcherTrails = new CatcherTrailDisplay(),
|
||||
comboDisplay = new CatchComboDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Bottom = 350f },
|
||||
X = CatchPlayfield.CENTER_X
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,6 +108,27 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
comboDisplay.X = Catcher.X;
|
||||
|
||||
if (Time.Elapsed <= 0)
|
||||
{
|
||||
// This is probably a wrong value, but currently the true value is not recorded.
|
||||
// Setting `true` will prevent generation of false-positive after-images (with more false-negatives).
|
||||
lastHyperDashState = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastHyperDashState && Catcher.HyperDashing)
|
||||
displayCatcherTrail(CatcherTrailAnimation.HyperDashAfterImage);
|
||||
|
||||
if (Catcher.Dashing || Catcher.HyperDashing)
|
||||
{
|
||||
double generationInterval = Catcher.HyperDashing ? 25 : 50;
|
||||
|
||||
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
|
||||
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
||||
}
|
||||
|
||||
lastHyperDashState = Catcher.HyperDashing;
|
||||
}
|
||||
|
||||
public void SetCatcherPosition(float X)
|
||||
@@ -154,5 +181,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void displayCatcherTrail(CatcherTrailAnimation animation) => catcherTrails.Add(new CatcherTrailEntry(Time.Current, Catcher.CurrentState, Catcher.X, Catcher.BodyScale, animation));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
@@ -12,13 +12,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// A trail of the catcher.
|
||||
/// It also represents a hyper dash afterimage.
|
||||
/// </summary>
|
||||
public class CatcherTrail : PoolableDrawable
|
||||
public class CatcherTrail : PoolableDrawableWithLifetime<CatcherTrailEntry>
|
||||
{
|
||||
public CatcherAnimationState AnimationState
|
||||
{
|
||||
set => body.AnimationState.Value = value;
|
||||
}
|
||||
|
||||
private readonly SkinnableCatcher body;
|
||||
|
||||
public CatcherTrail()
|
||||
@@ -34,11 +29,40 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
};
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
protected override void OnApply(CatcherTrailEntry entry)
|
||||
{
|
||||
Position = new Vector2(entry.Position, 0);
|
||||
Scale = entry.Scale;
|
||||
|
||||
body.AnimationState.Value = entry.CatcherState;
|
||||
|
||||
using (BeginAbsoluteSequence(entry.LifetimeStart, false))
|
||||
applyTransforms(entry.Animation);
|
||||
}
|
||||
|
||||
protected override void OnFree(CatcherTrailEntry entry)
|
||||
{
|
||||
ApplyTransformsAt(double.MinValue);
|
||||
ClearTransforms();
|
||||
Alpha = 1;
|
||||
base.FreeAfterUse();
|
||||
}
|
||||
|
||||
private void applyTransforms(CatcherTrailAnimation animation)
|
||||
{
|
||||
switch (animation)
|
||||
{
|
||||
case CatcherTrailAnimation.Dashing:
|
||||
case CatcherTrailAnimation.HyperDashing:
|
||||
this.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||
this.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
|
||||
this.ScaleTo(Scale * 0.95f).ScaleTo(Scale * 1.2f, 1200, Easing.In);
|
||||
this.FadeOut(1200);
|
||||
break;
|
||||
}
|
||||
|
||||
Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public enum CatcherTrailAnimation
|
||||
{
|
||||
Dashing,
|
||||
HyperDashing,
|
||||
HyperDashAfterImage
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
@@ -15,70 +17,32 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// Represents a component responsible for displaying
|
||||
/// the appropriate catcher trails when requested to.
|
||||
/// </summary>
|
||||
public class CatcherTrailDisplay : CompositeDrawable
|
||||
public class CatcherTrailDisplay : PooledDrawableWithLifetimeContainer<CatcherTrailEntry, CatcherTrail>
|
||||
{
|
||||
private readonly Catcher catcher;
|
||||
/// <summary>
|
||||
/// The most recent time a dash trail was added to this container.
|
||||
/// Only alive (not faded out) trails are considered.
|
||||
/// Returns <see cref="double.NegativeInfinity"/> if no dash trail is alive.
|
||||
/// </summary>
|
||||
public double LastDashTrailTime => getLastDashTrailTime();
|
||||
|
||||
public Color4 HyperDashTrailsColour => hyperDashTrails.Colour;
|
||||
|
||||
public Color4 HyperDashAfterImageColour => hyperDashAfterImages.Colour;
|
||||
|
||||
protected override bool RemoveRewoundEntry => true;
|
||||
|
||||
private readonly DrawablePool<CatcherTrail> trailPool;
|
||||
|
||||
private readonly Container<CatcherTrail> dashTrails;
|
||||
private readonly Container<CatcherTrail> hyperDashTrails;
|
||||
private readonly Container<CatcherTrail> endGlowSprites;
|
||||
private readonly Container<CatcherTrail> hyperDashAfterImages;
|
||||
|
||||
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
|
||||
public Color4 HyperDashTrailsColour
|
||||
public CatcherTrailDisplay()
|
||||
{
|
||||
get => hyperDashTrailsColour;
|
||||
set
|
||||
{
|
||||
if (hyperDashTrailsColour == value)
|
||||
return;
|
||||
|
||||
hyperDashTrailsColour = value;
|
||||
hyperDashTrails.Colour = hyperDashTrailsColour;
|
||||
}
|
||||
}
|
||||
|
||||
private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
|
||||
|
||||
public Color4 EndGlowSpritesColour
|
||||
{
|
||||
get => endGlowSpritesColour;
|
||||
set
|
||||
{
|
||||
if (endGlowSpritesColour == value)
|
||||
return;
|
||||
|
||||
endGlowSpritesColour = value;
|
||||
endGlowSprites.Colour = endGlowSpritesColour;
|
||||
}
|
||||
}
|
||||
|
||||
private bool trail;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to start displaying trails following the catcher.
|
||||
/// </summary>
|
||||
public bool DisplayTrail
|
||||
{
|
||||
get => trail;
|
||||
set
|
||||
{
|
||||
if (trail == value)
|
||||
return;
|
||||
|
||||
trail = value;
|
||||
|
||||
if (trail)
|
||||
displayTrail();
|
||||
}
|
||||
}
|
||||
|
||||
public CatcherTrailDisplay([NotNull] Catcher catcher)
|
||||
{
|
||||
this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher));
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
@@ -86,47 +50,86 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
trailPool = new DrawablePool<CatcherTrail>(30),
|
||||
dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both },
|
||||
hyperDashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||
endGlowSprites = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||
hyperDashAfterImages = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a single end-glow catcher sprite.
|
||||
/// </summary>
|
||||
public void DisplayEndGlow()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
var endGlow = createTrailSprite(endGlowSprites);
|
||||
base.LoadComplete();
|
||||
|
||||
endGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
|
||||
endGlow.ScaleTo(endGlow.Scale * 0.95f).ScaleTo(endGlow.Scale * 1.2f, 1200, Easing.In);
|
||||
endGlow.FadeOut(1200);
|
||||
endGlow.Expire(true);
|
||||
skin.SourceChanged += skinSourceChanged;
|
||||
skinSourceChanged();
|
||||
}
|
||||
|
||||
private void displayTrail()
|
||||
private void skinSourceChanged()
|
||||
{
|
||||
if (!DisplayTrail)
|
||||
return;
|
||||
|
||||
var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails);
|
||||
|
||||
sprite.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
|
||||
sprite.Expire(true);
|
||||
|
||||
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
|
||||
hyperDashTrails.Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? Catcher.DEFAULT_HYPER_DASH_COLOUR;
|
||||
hyperDashAfterImages.Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashTrails.Colour;
|
||||
}
|
||||
|
||||
private CatcherTrail createTrailSprite(Container<CatcherTrail> target)
|
||||
protected override void AddDrawable(CatcherTrailEntry entry, CatcherTrail drawable)
|
||||
{
|
||||
CatcherTrail sprite = trailPool.Get();
|
||||
switch (entry.Animation)
|
||||
{
|
||||
case CatcherTrailAnimation.Dashing:
|
||||
dashTrails.Add(drawable);
|
||||
break;
|
||||
|
||||
sprite.AnimationState = catcher.CurrentState;
|
||||
sprite.Scale = catcher.Scale * catcher.Body.Scale;
|
||||
sprite.Position = catcher.Position;
|
||||
case CatcherTrailAnimation.HyperDashing:
|
||||
hyperDashTrails.Add(drawable);
|
||||
break;
|
||||
|
||||
target.Add(sprite);
|
||||
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||
hyperDashAfterImages.Add(drawable);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sprite;
|
||||
protected override void RemoveDrawable(CatcherTrailEntry entry, CatcherTrail drawable)
|
||||
{
|
||||
switch (entry.Animation)
|
||||
{
|
||||
case CatcherTrailAnimation.Dashing:
|
||||
dashTrails.Remove(drawable);
|
||||
break;
|
||||
|
||||
case CatcherTrailAnimation.HyperDashing:
|
||||
hyperDashTrails.Remove(drawable);
|
||||
break;
|
||||
|
||||
case CatcherTrailAnimation.HyperDashAfterImage:
|
||||
hyperDashAfterImages.Remove(drawable);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override CatcherTrail GetDrawable(CatcherTrailEntry entry)
|
||||
{
|
||||
CatcherTrail trail = trailPool.Get();
|
||||
trail.Apply(entry);
|
||||
return trail;
|
||||
}
|
||||
|
||||
private double getLastDashTrailTime()
|
||||
{
|
||||
double maxTime = double.NegativeInfinity;
|
||||
|
||||
foreach (var trail in dashTrails)
|
||||
maxTime = Math.Max(maxTime, trail.LifetimeStart);
|
||||
|
||||
foreach (var trail in hyperDashTrails)
|
||||
maxTime = Math.Max(maxTime, trail.LifetimeStart);
|
||||
|
||||
return maxTime;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin != null)
|
||||
skin.SourceChanged -= skinSourceChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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 osu.Framework.Graphics.Performance;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class CatcherTrailEntry : LifetimeEntry
|
||||
{
|
||||
public readonly CatcherAnimationState CatcherState;
|
||||
|
||||
public readonly float Position;
|
||||
|
||||
/// <summary>
|
||||
/// The scaling of the catcher body. It also represents a flipped catcher (negative x component).
|
||||
/// </summary>
|
||||
public readonly Vector2 Scale;
|
||||
|
||||
public readonly CatcherTrailAnimation Animation;
|
||||
|
||||
public CatcherTrailEntry(double startTime, CatcherAnimationState catcherState, float position, Vector2 scale, CatcherTrailAnimation animation)
|
||||
{
|
||||
LifetimeStart = startTime;
|
||||
CatcherState = catcherState;
|
||||
Position = position;
|
||||
Scale = scale;
|
||||
Animation = animation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +1,56 @@
|
||||
// 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 osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry>
|
||||
{
|
||||
private readonly CircularContainer largeFaint;
|
||||
private readonly CircularContainer smallFaint;
|
||||
private readonly CircularContainer directionalGlow1;
|
||||
private readonly CircularContainer directionalGlow2;
|
||||
private readonly SkinnableDrawable skinnableExplosion;
|
||||
|
||||
public HitExplosion()
|
||||
{
|
||||
Size = new Vector2(20);
|
||||
Anchor = Anchor.TopCentre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
|
||||
// scale roughly in-line with visual appearance of notes
|
||||
const float initial_height = 10;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
InternalChild = skinnableExplosion = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
|
||||
{
|
||||
largeFaint = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
smallFaint = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
directionalGlow1 = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Size = new Vector2(0.01f, initial_height),
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
directionalGlow2 = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Size = new Vector2(0.01f, initial_height),
|
||||
Blending = BlendingParameters.Additive,
|
||||
}
|
||||
CentreComponent = false,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnApply(HitExplosionEntry entry)
|
||||
{
|
||||
X = entry.Position;
|
||||
Scale = new Vector2(entry.Scale);
|
||||
setColour(entry.ObjectColour);
|
||||
|
||||
using (BeginAbsoluteSequence(entry.LifetimeStart))
|
||||
applyTransforms(entry.RNGSeed);
|
||||
base.OnApply(entry);
|
||||
if (IsLoaded)
|
||||
apply(entry);
|
||||
}
|
||||
|
||||
private void applyTransforms(int randomSeed)
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
apply(Entry);
|
||||
}
|
||||
|
||||
private void apply(HitExplosionEntry? entry)
|
||||
{
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
ApplyTransformsAt(double.MinValue, true);
|
||||
ClearTransforms(true);
|
||||
|
||||
const double duration = 400;
|
||||
|
||||
// we want our size to be very small so the glow dominates it.
|
||||
largeFaint.Size = new Vector2(0.8f);
|
||||
largeFaint
|
||||
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
|
||||
.FadeOut(duration * 2);
|
||||
|
||||
const float angle_variangle = 15; // should be less than 45
|
||||
directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
|
||||
directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
|
||||
|
||||
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
|
||||
}
|
||||
|
||||
private void setColour(Color4 objectColour)
|
||||
{
|
||||
const float roundness = 100;
|
||||
|
||||
largeFaint.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
|
||||
Roundness = 160,
|
||||
Radius = 200,
|
||||
};
|
||||
|
||||
smallFaint.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
|
||||
Roundness = 20,
|
||||
Radius = 50,
|
||||
};
|
||||
|
||||
directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
|
||||
Roundness = roundness,
|
||||
Radius = 40,
|
||||
};
|
||||
(skinnableExplosion.Drawable as IHitExplosion)?.Animate(entry);
|
||||
LifetimeEnd = skinnableExplosion.Drawable.LatestTransformEndTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
|
||||
@@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public HitExplosionContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(pool = new DrawablePool<HitExplosion>(10));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,24 +2,42 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osuTK.Graphics;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class HitExplosionEntry : LifetimeEntry
|
||||
{
|
||||
public readonly float Position;
|
||||
public readonly float Scale;
|
||||
public readonly Color4 ObjectColour;
|
||||
public readonly int RNGSeed;
|
||||
/// <summary>
|
||||
/// The judgement result that triggered this explosion.
|
||||
/// </summary>
|
||||
public JudgementResult JudgementResult { get; }
|
||||
|
||||
public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed)
|
||||
/// <summary>
|
||||
/// The hitobject which triggered this explosion.
|
||||
/// </summary>
|
||||
public CatchHitObject HitObject => (CatchHitObject)JudgementResult.HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The accent colour of the object caught.
|
||||
/// </summary>
|
||||
public Color4 ObjectColour { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The position at which the object was caught.
|
||||
/// </summary>
|
||||
public float Position { get; }
|
||||
|
||||
public HitExplosionEntry(double startTime, JudgementResult judgementResult, Color4 objectColour, float position)
|
||||
{
|
||||
LifetimeStart = startTime;
|
||||
Position = position;
|
||||
Scale = scale;
|
||||
JudgementResult = judgementResult;
|
||||
ObjectColour = objectColour;
|
||||
RNGSeed = rngSeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Common interface for all hit explosion skinnables.
|
||||
/// </summary>
|
||||
public interface IHitExplosion
|
||||
{
|
||||
/// <summary>
|
||||
/// Begins animating this <see cref="IHitExplosion"/>.
|
||||
/// </summary>
|
||||
void Animate(HitExplosionEntry entry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public class TestSceneManiaComposeScreen : EditorClockTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup compose screen", () =>
|
||||
{
|
||||
var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
};
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
(typeof(EditorBeatmap), editorBeatmap),
|
||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||
},
|
||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for composer", () => this.ChildrenOfType<HitObjectComposer>().SingleOrDefault()?.IsLoaded == true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultSkin()
|
||||
{
|
||||
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacySkin()
|
||||
{
|
||||
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
{
|
||||
public class Strain : StrainSkill
|
||||
public class Strain : StrainDecaySkill
|
||||
{
|
||||
private const double individual_decay_base = 0.125;
|
||||
private const double overall_decay_base = 0.30;
|
||||
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
return individualStrain + overallStrain - CurrentStrain;
|
||||
}
|
||||
|
||||
protected override double GetPeakStrain(double offset)
|
||||
protected override double CalculateInitialStrain(double offset)
|
||||
=> applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base);
|
||||
|
||||
|
||||
@@ -253,7 +253,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
case ModType.Fun:
|
||||
return new Mod[]
|
||||
{
|
||||
new MultiMod(new ModWindUp(), new ModWindDown())
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new ManiaModMuted(),
|
||||
};
|
||||
|
||||
default:
|
||||
|
||||
@@ -10,13 +10,9 @@ using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModMirror : Mod, IApplicableToBeatmap
|
||||
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
|
||||
{
|
||||
public override string Name => "Mirror";
|
||||
public override string Acronym => "MR";
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override string Description => "Notes are flipped horizontally.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModMuted : ModMuted<ManiaHitObject>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -275,9 +275,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
return false;
|
||||
|
||||
beginHoldAt(Time.Current - Head.HitObject.StartTime);
|
||||
Head.UpdateResult();
|
||||
|
||||
return true;
|
||||
return Head.UpdateResult();
|
||||
}
|
||||
|
||||
private void beginHoldAt(double timeOffset)
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
Origin = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
public void UpdateResult() => base.UpdateResult(true);
|
||||
public bool UpdateResult() => base.UpdateResult(true);
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("last connection displayed", () =>
|
||||
{
|
||||
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300));
|
||||
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
|
||||
return lastConnection.DrawWidth > 50;
|
||||
});
|
||||
}
|
||||
@@ -166,14 +166,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
Vector2 position = slider.Path.ControlPoints[index].Position.Value;
|
||||
Vector2 position = slider.Path.ControlPoints[index].Position;
|
||||
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertControlPointPathType(int controlPointIndex, PathType? type)
|
||||
{
|
||||
AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type);
|
||||
AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type == type);
|
||||
}
|
||||
|
||||
private void addContextMenuItemStep(string contextMenuText)
|
||||
|
||||
@@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestDragControlPointPathAfterChangingType()
|
||||
{
|
||||
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier);
|
||||
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.Bezier);
|
||||
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
|
||||
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve);
|
||||
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PerfectCurve);
|
||||
|
||||
moveMouseToControlPoint(4);
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
@@ -137,15 +137,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
|
||||
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
|
||||
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type);
|
||||
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type == type);
|
||||
|
||||
private void assertControlPointPosition(int index, Vector2 position) =>
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1));
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position, 1));
|
||||
|
||||
private class TestSliderBlueprint : SliderSelectionBlueprint
|
||||
{
|
||||
|
||||
@@ -385,10 +385,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected);
|
||||
|
||||
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type.Value == type);
|
||||
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type == type);
|
||||
|
||||
private void assertControlPointPosition(int index, Vector2 position) =>
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1));
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1));
|
||||
|
||||
private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
|
||||
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
|
||||
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public class TestSceneOsuModMuted : OsuModTestScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures that a final volume combo of 0 (i.e. "always muted" mode) constantly plays metronome and completely mutes track.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestZeroFinalCombo() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModMuted
|
||||
{
|
||||
MuteComboCount = { Value = 0 },
|
||||
},
|
||||
PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 &&
|
||||
Player.ChildrenOfType<Metronome>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that copying from a normal mod with 0 final combo while originally inversed does not yield incorrect results.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestModCopy()
|
||||
{
|
||||
OsuModMuted muted = null;
|
||||
|
||||
AddStep("create inversed mod", () => muted = new OsuModMuted
|
||||
{
|
||||
MuteComboCount = { Value = 100 },
|
||||
InverseMuting = { Value = true },
|
||||
});
|
||||
|
||||
AddStep("copy from normal", () => muted.CopyFrom(new OsuModMuted
|
||||
{
|
||||
MuteComboCount = { Value = 0 },
|
||||
InverseMuting = { Value = false },
|
||||
}));
|
||||
|
||||
AddAssert("mute combo count = 0", () => muted.MuteComboCount.Value == 0);
|
||||
AddAssert("inverse muting = false", () => muted.InverseMuting.Value == false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
public abstract class OsuStrainSkill : StrainSkill
|
||||
public abstract class OsuStrainSkill : StrainDecaySkill
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
private void updateConnectingPath()
|
||||
{
|
||||
Position = slider.StackedPosition + ControlPoint.Position.Value;
|
||||
Position = slider.StackedPosition + ControlPoint.Position;
|
||||
|
||||
path.ClearVertices();
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
return;
|
||||
|
||||
path.AddVertex(Vector2.Zero);
|
||||
path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value);
|
||||
path.AddVertex(slider.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
|
||||
|
||||
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<float> sliderScale;
|
||||
private IBindable<Vector2> controlPointPosition;
|
||||
|
||||
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
|
||||
{
|
||||
@@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
updatePathType();
|
||||
});
|
||||
|
||||
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
|
||||
controlPoint.Changed += updateMarkerDisplay;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@@ -117,9 +116,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
sliderPosition = slider.PositionBindable.GetBoundCopy();
|
||||
sliderPosition.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
controlPointPosition = ControlPoint.Position.GetBoundCopy();
|
||||
controlPointPosition.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
sliderScale = slider.ScaleBindable.GetBoundCopy();
|
||||
sliderScale.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
@@ -174,8 +170,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
dragStartPosition = ControlPoint.Position.Value;
|
||||
dragPathType = PointsInSegment[0].Type.Value;
|
||||
dragStartPosition = ControlPoint.Position;
|
||||
dragPathType = PointsInSegment[0].Type;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
@@ -186,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position.Value).ToArray();
|
||||
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
var oldPosition = slider.Position;
|
||||
var oldStartTime = slider.StartTime;
|
||||
|
||||
@@ -202,15 +198,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
|
||||
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position.Value -= movementDelta;
|
||||
slider.Path.ControlPoints[i].Position -= movementDelta;
|
||||
}
|
||||
else
|
||||
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
|
||||
ControlPoint.Position = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
|
||||
|
||||
if (!slider.Path.HasValidLength)
|
||||
{
|
||||
for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position.Value = oldControlPoints[i];
|
||||
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
|
||||
slider.Position = oldPosition;
|
||||
slider.StartTime = oldStartTime;
|
||||
@@ -218,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
|
||||
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
|
||||
PointsInSegment[0].Type.Value = dragPathType;
|
||||
PointsInSegment[0].Type = dragPathType;
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
|
||||
@@ -230,19 +226,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
private void updatePathType()
|
||||
{
|
||||
if (ControlPoint.Type.Value != PathType.PerfectCurve)
|
||||
if (ControlPoint.Type != PathType.PerfectCurve)
|
||||
return;
|
||||
|
||||
if (PointsInSegment.Count > 3)
|
||||
ControlPoint.Type.Value = PathType.Bezier;
|
||||
ControlPoint.Type = PathType.Bezier;
|
||||
|
||||
if (PointsInSegment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position.Value).ToArray();
|
||||
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
ControlPoint.Type.Value = PathType.Bezier;
|
||||
ControlPoint.Type = PathType.Bezier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -250,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
private void updateMarkerDisplay()
|
||||
{
|
||||
Position = slider.StackedPosition + ControlPoint.Position.Value;
|
||||
Position = slider.StackedPosition + ControlPoint.Position;
|
||||
|
||||
markerRing.Alpha = IsSelected.Value ? 1 : 0;
|
||||
|
||||
@@ -265,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private Color4 getColourFromNodeType()
|
||||
{
|
||||
if (!(ControlPoint.Type.Value is PathType pathType))
|
||||
if (!(ControlPoint.Type is PathType pathType))
|
||||
return colours.Yellow;
|
||||
|
||||
switch (pathType)
|
||||
@@ -284,6 +280,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
|
||||
public LocalisableString TooltipText => ControlPoint.Type.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -173,12 +173,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
|
||||
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
|
||||
piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value;
|
||||
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
piece.ControlPoint.Type.Value = type;
|
||||
piece.ControlPoint.Type = type;
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
@@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private MenuItem createMenuItemForPathType(PathType? type)
|
||||
{
|
||||
int totalCount = Pieces.Count(p => p.IsSelected.Value);
|
||||
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
|
||||
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
|
||||
|
||||
var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
|
||||
{
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
Debug.Assert(lastPoint != null);
|
||||
|
||||
segmentStart = lastPoint;
|
||||
segmentStart.Type.Value = PathType.Linear;
|
||||
segmentStart.Type = PathType.Linear;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
}
|
||||
@@ -153,15 +153,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
case 1:
|
||||
case 2:
|
||||
segmentStart.Type.Value = PathType.Linear;
|
||||
segmentStart.Type = PathType.Linear;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
segmentStart.Type.Value = PathType.PerfectCurve;
|
||||
segmentStart.Type = PathType.PerfectCurve;
|
||||
break;
|
||||
|
||||
default:
|
||||
segmentStart.Type.Value = PathType.Bezier;
|
||||
segmentStart.Type = PathType.Bezier;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// The cursor does not overlap a previous control point, so it can be added if not already existing.
|
||||
if (cursor == null)
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
|
||||
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = Vector2.Zero });
|
||||
|
||||
// The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
|
||||
currentSegmentLength++;
|
||||
@@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
// Update the cursor position.
|
||||
cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
}
|
||||
else if (cursor != null)
|
||||
{
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
Debug.Assert(placementControlPointIndex != null);
|
||||
|
||||
HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position.Value = e.MousePosition - HitObject.Position;
|
||||
HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position = e.MousePosition - HitObject.Position;
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
@@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
for (int i = 0; i < controlPoints.Count - 1; i++)
|
||||
{
|
||||
float dist = new Line(controlPoints[i].Position.Value, controlPoints[i + 1].Position.Value).DistanceToPoint(position);
|
||||
float dist = new Line(controlPoints[i].Position, controlPoints[i + 1].Position).DistanceToPoint(position);
|
||||
|
||||
if (dist < minDistance)
|
||||
{
|
||||
@@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
// Move the control points from the insertion index onwards to make room for the insertion
|
||||
controlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } });
|
||||
controlPoints.Insert(insertionIndex, new PathControlPoint { Position = position });
|
||||
|
||||
return insertionIndex;
|
||||
}
|
||||
@@ -207,8 +207,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
// The first control point in the slider must have a type, so take it from the previous "first" one
|
||||
// Todo: Should be handled within SliderPath itself
|
||||
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type.Value == null)
|
||||
controlPoints[1].Type.Value = controlPoints[0].Type.Value;
|
||||
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
|
||||
controlPoints[1].Type = controlPoints[0].Type;
|
||||
|
||||
controlPoints.Remove(c);
|
||||
}
|
||||
@@ -222,9 +222,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
// The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
|
||||
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
|
||||
Vector2 first = controlPoints[0].Position.Value;
|
||||
Vector2 first = controlPoints[0].Position;
|
||||
foreach (var c in controlPoints)
|
||||
c.Position.Value -= first;
|
||||
c.Position -= first;
|
||||
HitObject.Position += first;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
protected override GameplayCursorContainer CreateCursor() => null;
|
||||
|
||||
public OsuEditorPlayfield()
|
||||
{
|
||||
HitPolicy = new AnyOrderHitPolicy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
@@ -59,11 +64,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (hitObject is DrawableHitCircle circle)
|
||||
{
|
||||
circle.ApproachCircle
|
||||
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
|
||||
.Expire();
|
||||
using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime))
|
||||
{
|
||||
circle.ApproachCircle
|
||||
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
|
||||
.Expire();
|
||||
|
||||
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
|
||||
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
if (hitObject is IHasMainCirclePiece mainPieceContainer)
|
||||
|
||||
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
{
|
||||
point.Position.Value = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
|
||||
point.Position = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * point.Position.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * point.Position.Y
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
foreach (var point in path.Path.ControlPoints)
|
||||
point.Position.Value = RotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
|
||||
point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,9 +163,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale)
|
||||
{
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
||||
|
||||
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
|
||||
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
|
||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
||||
@@ -178,13 +178,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
{
|
||||
oldControlPoints.Enqueue(point.Position.Value);
|
||||
point.Position.Value *= pathRelativeDeltaScale;
|
||||
oldControlPoints.Enqueue(point.Position);
|
||||
point.Position *= pathRelativeDeltaScale;
|
||||
}
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
|
||||
slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i];
|
||||
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
||||
@@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return;
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
point.Position.Value = oldControlPoints.Dequeue();
|
||||
point.Position = oldControlPoints.Dequeue();
|
||||
}
|
||||
|
||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap));
|
||||
drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield, drawableRuleset.Beatmap));
|
||||
}
|
||||
|
||||
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
|
||||
@@ -128,8 +129,21 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
float start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
|
||||
float end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
|
||||
float start, end;
|
||||
|
||||
if (Precision.AlmostEquals(restrictTo.Rotation, 0))
|
||||
{
|
||||
start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
|
||||
end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
|
||||
}
|
||||
else
|
||||
{
|
||||
float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent).X;
|
||||
float halfDiagonal = (restrictTo.DrawSize / 2).LengthFast;
|
||||
|
||||
start = center - halfDiagonal;
|
||||
end = center + halfDiagonal;
|
||||
}
|
||||
|
||||
float rawWidth = end - start;
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// 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.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@@ -15,23 +14,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override double ScoreMultiplier => 1.06;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
var osuObject = (OsuHitObject)hitObject;
|
||||
|
||||
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
|
||||
|
||||
if (!(hitObject is Slider slider))
|
||||
return;
|
||||
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModMirror : ModMirror, IApplicableToHitObject
|
||||
{
|
||||
public override string Description => "Flip objects on the chosen axes.";
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
|
||||
|
||||
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
|
||||
public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>();
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
var osuObject = (OsuHitObject)hitObject;
|
||||
|
||||
switch (Reflection.Value)
|
||||
{
|
||||
case MirrorType.Horizontal:
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
|
||||
break;
|
||||
|
||||
case MirrorType.Vertical:
|
||||
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||
break;
|
||||
|
||||
case MirrorType.Both:
|
||||
OsuHitObjectGenerationUtils.ReflectHorizontally(osuObject);
|
||||
OsuHitObjectGenerationUtils.ReflectVertically(osuObject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public enum MirrorType
|
||||
{
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Both
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModMuted : ModMuted<OsuHitObject>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -16,7 +14,6 @@ using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -29,7 +26,6 @@ using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -67,11 +63,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
/// </summary>
|
||||
private const float distance_cap = 380f;
|
||||
|
||||
// The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle.
|
||||
// The closer the hit objects draw to the border, the sharper the turn
|
||||
private const byte border_distance_x = 192;
|
||||
private const byte border_distance_y = 144;
|
||||
|
||||
/// <summary>
|
||||
/// The extent of rotation towards playfield centre when a circle is near the edge
|
||||
/// </summary>
|
||||
@@ -341,46 +332,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Overlays.Add(new TargetBeatContainer(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
}
|
||||
|
||||
public class TargetBeatContainer : BeatSyncedContainer
|
||||
{
|
||||
private readonly double firstHitTime;
|
||||
|
||||
private PausableSkinnableSound sample;
|
||||
|
||||
public TargetBeatContainer(double firstHitTime)
|
||||
{
|
||||
this.firstHitTime = firstHitTime;
|
||||
AllowMistimedEventFiring = false;
|
||||
Divisor = 1;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
sample = new PausableSkinnableSound(new SampleInfo("Gameplay/catch-banana"))
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||
|
||||
if (!IsBeatSyncedWithTrack) return;
|
||||
|
||||
int timeSignature = (int)timingPoint.TimeSignature;
|
||||
|
||||
// play metronome from one measure before the first object.
|
||||
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
|
||||
return;
|
||||
|
||||
sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;
|
||||
sample.Play();
|
||||
}
|
||||
drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -20,8 +21,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
{
|
||||
Start = start;
|
||||
LifetimeStart = Start.StartTime;
|
||||
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
private OsuHitObject? end;
|
||||
@@ -41,31 +40,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
}
|
||||
}
|
||||
|
||||
private bool wasBound;
|
||||
|
||||
private void bindEvents()
|
||||
{
|
||||
UnbindEvents();
|
||||
|
||||
if (End == null)
|
||||
return;
|
||||
|
||||
// Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
|
||||
Start.DefaultsApplied += onDefaultsApplied;
|
||||
Start.PositionBindable.ValueChanged += onPositionChanged;
|
||||
|
||||
if (End != null)
|
||||
{
|
||||
End.DefaultsApplied += onDefaultsApplied;
|
||||
End.PositionBindable.ValueChanged += onPositionChanged;
|
||||
}
|
||||
End.DefaultsApplied += onDefaultsApplied;
|
||||
End.PositionBindable.ValueChanged += onPositionChanged;
|
||||
|
||||
wasBound = true;
|
||||
}
|
||||
|
||||
public void UnbindEvents()
|
||||
{
|
||||
if (!wasBound)
|
||||
return;
|
||||
|
||||
Debug.Assert(End != null);
|
||||
|
||||
Start.DefaultsApplied -= onDefaultsApplied;
|
||||
Start.PositionBindable.ValueChanged -= onPositionChanged;
|
||||
|
||||
if (End != null)
|
||||
{
|
||||
End.DefaultsApplied -= onDefaultsApplied;
|
||||
End.PositionBindable.ValueChanged -= onPositionChanged;
|
||||
}
|
||||
End.DefaultsApplied -= onDefaultsApplied;
|
||||
End.PositionBindable.ValueChanged -= onPositionChanged;
|
||||
|
||||
wasBound = false;
|
||||
}
|
||||
|
||||
private void onDefaultsApplied(HitObject obj) => refreshLifetimes();
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.PlayAnimation();
|
||||
|
||||
if (Result != HitResult.Miss)
|
||||
JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
|
||||
JudgementText.ScaleTo(new Vector2(0.8f, 1)).Then().ScaleTo(new Vector2(1.2f, 1), 1800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value)));
|
||||
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
|
||||
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModDifficultyAdjust(),
|
||||
new OsuModClassic(),
|
||||
new OsuModRandom(),
|
||||
new OsuModMirror(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
@@ -188,6 +189,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModTraceable(),
|
||||
new OsuModBarrelRoll(),
|
||||
new OsuModApproachDifferent(),
|
||||
new OsuModMuted(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
@@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
private double lastTrailTime;
|
||||
private IBindable<float> cursorSize;
|
||||
|
||||
private Vector2? currentPosition;
|
||||
|
||||
public LegacyCursorTrail(ISkin skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
@@ -54,22 +57,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
}
|
||||
|
||||
protected override double FadeDuration => disjointTrail ? 150 : 500;
|
||||
protected override float FadeExponent => 1;
|
||||
|
||||
protected override bool InterpolateMovements => !disjointTrail;
|
||||
|
||||
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
|
||||
protected override bool AvoidDrawingNearCursor => !disjointTrail;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!disjointTrail || !currentPosition.HasValue)
|
||||
return;
|
||||
|
||||
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
|
||||
{
|
||||
lastTrailTime = Time.Current;
|
||||
AddTrail(currentPosition.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
if (!disjointTrail)
|
||||
return base.OnMouseMove(e);
|
||||
|
||||
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
|
||||
{
|
||||
lastTrailTime = Time.Current;
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
currentPosition = e.ScreenSpaceMousePosition;
|
||||
|
||||
// Intentionally block the base call as we're adding the trails ourselves.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
// Roughly matches osu!stable's slider border portions.
|
||||
=> base.CalculatedBorderPortion * 0.77f;
|
||||
|
||||
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f);
|
||||
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.7f);
|
||||
|
||||
protected override Color4 ColourAt(float position)
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Color4 outerColour = AccentColour.Darken(0.1f);
|
||||
Color4 innerColour = lighten(AccentColour, 0.5f);
|
||||
|
||||
return Interpolation.ValueAt(position / realGradientPortion, outerColour, innerColour, 0, 1);
|
||||
return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IHitPolicy"/> which allows hitobjects to be hit in any order.
|
||||
/// </summary>
|
||||
public class AnyOrderHitPolicy : IHitPolicy
|
||||
{
|
||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
||||
|
||||
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
|
||||
|
||||
public void HandleHit(DrawableHitObject hitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
private const int max_sprites = 2048;
|
||||
|
||||
/// <summary>
|
||||
/// An exponentiating factor to ease the trail fade.
|
||||
/// </summary>
|
||||
protected virtual float FadeExponent => 1.7f;
|
||||
|
||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||
private int currentIndex;
|
||||
private IShader shader;
|
||||
@@ -133,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
protected virtual bool InterpolateMovements => true;
|
||||
|
||||
protected virtual float IntervalMultiplier => 1.0f;
|
||||
protected virtual bool AvoidDrawingNearCursor => false;
|
||||
|
||||
private Vector2? lastPosition;
|
||||
private readonly InputResampler resampler = new InputResampler();
|
||||
@@ -141,43 +147,45 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
Vector2 pos = e.ScreenSpaceMousePosition;
|
||||
AddTrail(e.ScreenSpaceMousePosition);
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
if (lastPosition == null)
|
||||
protected void AddTrail(Vector2 position)
|
||||
{
|
||||
if (InterpolateMovements)
|
||||
{
|
||||
lastPosition = pos;
|
||||
resampler.AddPosition(lastPosition.Value);
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
foreach (Vector2 pos2 in resampler.AddPosition(pos))
|
||||
{
|
||||
Trace.Assert(lastPosition.HasValue);
|
||||
|
||||
if (InterpolateMovements)
|
||||
if (!lastPosition.HasValue)
|
||||
{
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
lastPosition = position;
|
||||
resampler.AddPosition(lastPosition.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Vector2 pos2 in resampler.AddPosition(position))
|
||||
{
|
||||
Trace.Assert(lastPosition.HasValue);
|
||||
|
||||
Vector2 pos1 = lastPosition.Value;
|
||||
Vector2 diff = pos2 - pos1;
|
||||
float distance = diff.Length;
|
||||
Vector2 direction = diff / distance;
|
||||
|
||||
float interval = partSize.X / 2.5f * IntervalMultiplier;
|
||||
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
|
||||
|
||||
for (float d = interval; d < distance; d += interval)
|
||||
for (float d = interval; d < stopAt; d += interval)
|
||||
{
|
||||
lastPosition = pos1 + direction * d;
|
||||
addPart(lastPosition.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lastPosition = pos2;
|
||||
addPart(lastPosition.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnMouseMove(e);
|
||||
else
|
||||
{
|
||||
lastPosition = position;
|
||||
addPart(lastPosition.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void addPart(Vector2 screenSpacePosition)
|
||||
@@ -206,10 +214,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private Texture texture;
|
||||
|
||||
private float time;
|
||||
private float fadeExponent;
|
||||
|
||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||
private Vector2 size;
|
||||
|
||||
private Vector2 originPosition;
|
||||
|
||||
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
|
||||
@@ -227,6 +235,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
texture = Source.texture;
|
||||
size = Source.partSize;
|
||||
time = Source.time;
|
||||
fadeExponent = Source.FadeExponent;
|
||||
|
||||
originPosition = Vector2.Zero;
|
||||
|
||||
@@ -249,6 +258,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
shader.Bind();
|
||||
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
|
||||
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
|
||||
|
||||
texture.TextureGL.Bind();
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
private void onJudgementLoaded(DrawableOsuJudgement judgement)
|
||||
{
|
||||
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
|
||||
judgementAboveHitObjectLayer.Add(judgement.ProxiedAboveHitObjectsContent);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
@@ -150,6 +150,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
|
||||
|
||||
judgementLayer.Add(explosion);
|
||||
|
||||
// the proxied content is added to judgementAboveHitObjectLayer once, on first load, and never removed from it.
|
||||
// ensure that ordering is consistent with expectations (latest judgement should be front-most).
|
||||
judgementAboveHitObjectLayer.ChangeChildDepth(explosion.ProxiedAboveHitObjectsContent, (float)-result.TimeAbsolute);
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Utils
|
||||
@@ -100,5 +104,47 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
initial.Length * MathF.Sin(finalAngleRad)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield horizontally.
|
||||
/// </summary>
|
||||
/// <param name="osuObject">The object to reflect.</param>
|
||||
public static void ReflectHorizontally(OsuHitObject osuObject)
|
||||
{
|
||||
osuObject.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - osuObject.X, osuObject.Position.Y);
|
||||
|
||||
if (!(osuObject is Slider slider))
|
||||
return;
|
||||
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = new Vector2(-point.Position.X, point.Position.Y);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield vertically.
|
||||
/// </summary>
|
||||
/// <param name="osuObject">The object to reflect.</param>
|
||||
public static void ReflectVertically(OsuHitObject osuObject)
|
||||
{
|
||||
osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
|
||||
|
||||
if (!(osuObject is Slider slider))
|
||||
return;
|
||||
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = new Vector2(point.Position.X, -point.Position.Y);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Calculates the colour coefficient of taiko difficulty.
|
||||
/// </summary>
|
||||
public class Colour : StrainSkill
|
||||
public class Colour : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Calculates the rhythm coefficient of taiko difficulty.
|
||||
/// </summary>
|
||||
public class Rhythm : StrainSkill
|
||||
public class Rhythm : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 10;
|
||||
protected override double StrainDecayBase => 0;
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// <remarks>
|
||||
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
|
||||
/// </remarks>
|
||||
public class Stamina : StrainSkill
|
||||
public class Stamina : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
@@ -2,10 +2,30 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModClassic : ModClassic
|
||||
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield
|
||||
{
|
||||
private DrawableTaikoRuleset drawableTaikoRuleset;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
// Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
||||
const float scroll_rate = 10;
|
||||
|
||||
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
|
||||
float ratio = drawableTaikoRuleset.DrawHeight / 480;
|
||||
|
||||
drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,23 +12,11 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModHidden : ModHidden, IApplicableToDifficulty
|
||||
public class TaikoModHidden : ModHidden
|
||||
{
|
||||
public override string Description => @"Beats fade out before you hit them!";
|
||||
public override double ScoreMultiplier => 1.06;
|
||||
|
||||
/// <summary>
|
||||
/// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter
|
||||
/// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the
|
||||
/// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1.
|
||||
/// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead,
|
||||
/// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3.
|
||||
/// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens.
|
||||
/// </summary>
|
||||
private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0);
|
||||
|
||||
private double originalSliderMultiplier;
|
||||
|
||||
private ControlPointInfo controlPointInfo;
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
@@ -41,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
|
||||
double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
|
||||
|
||||
return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
|
||||
return speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
|
||||
}
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
@@ -69,22 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
}
|
||||
|
||||
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
// needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted).
|
||||
originalSliderMultiplier = difficulty.SliderMultiplier;
|
||||
|
||||
// osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size.
|
||||
// This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio.
|
||||
// For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable.
|
||||
// Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time.
|
||||
difficulty.SliderMultiplier /= hd_sv_scale;
|
||||
}
|
||||
|
||||
public override void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
controlPointInfo = beatmap.ControlPointInfo;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModMuted : ModMuted<TaikoHitObject>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,8 @@ namespace osu.Game.Rulesets.Taiko
|
||||
case ModType.Fun:
|
||||
return new Mod[]
|
||||
{
|
||||
new MultiMod(new ModWindUp(), new ModWindDown())
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new TaikoModMuted(),
|
||||
};
|
||||
|
||||
default:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@@ -24,12 +25,14 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public class DrawableTaikoRuleset : DrawableScrollingRuleset<TaikoHitObject>
|
||||
{
|
||||
private SkinnableDrawable scroller;
|
||||
public new BindableDouble TimeRange => base.TimeRange;
|
||||
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
private SkinnableDrawable scroller;
|
||||
|
||||
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
/// <summary>
|
||||
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
|
||||
/// </summary>
|
||||
public const float DEFAULT_HEIGHT = 178;
|
||||
public const float DEFAULT_HEIGHT = 212;
|
||||
|
||||
private Container<HitExplosion> hitExplosionContainer;
|
||||
private Container<KiaiHitExplosion> kiaiExplosionContainer;
|
||||
|
||||
@@ -1,56 +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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
[TestFixture]
|
||||
public class BeatmapDifficultyCacheTest
|
||||
{
|
||||
[Test]
|
||||
public void TestKeyEqualsWithDifferentModInstances()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyEqualsWithDifferentModOrder()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
}
|
||||
|
||||
[TestCase(1.3, DifficultyRating.Easy)]
|
||||
[TestCase(1.993, DifficultyRating.Easy)]
|
||||
[TestCase(1.998, DifficultyRating.Normal)]
|
||||
[TestCase(2.4, DifficultyRating.Normal)]
|
||||
[TestCase(2.693, DifficultyRating.Normal)]
|
||||
[TestCase(2.698, DifficultyRating.Hard)]
|
||||
[TestCase(3.5, DifficultyRating.Hard)]
|
||||
[TestCase(3.993, DifficultyRating.Hard)]
|
||||
[TestCase(3.997, DifficultyRating.Insane)]
|
||||
[TestCase(5.0, DifficultyRating.Insane)]
|
||||
[TestCase(5.292, DifficultyRating.Insane)]
|
||||
[TestCase(5.297, DifficultyRating.Expert)]
|
||||
[TestCase(6.2, DifficultyRating.Expert)]
|
||||
[TestCase(6.493, DifficultyRating.Expert)]
|
||||
[TestCase(6.498, DifficultyRating.ExpertPlus)]
|
||||
[TestCase(8.3, DifficultyRating.ExpertPlus)]
|
||||
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
|
||||
{
|
||||
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
|
||||
|
||||
Assert.AreEqual(expectedBracket, actualBracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,12 +58,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile);
|
||||
Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
|
||||
Assert.AreEqual(164471, metadata.PreviewTime);
|
||||
Assert.IsFalse(beatmapInfo.Countdown);
|
||||
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
|
||||
Assert.IsTrue(beatmapInfo.RulesetID == 0);
|
||||
Assert.IsFalse(beatmapInfo.LetterboxInBreaks);
|
||||
Assert.IsFalse(beatmapInfo.SpecialStyle);
|
||||
Assert.IsFalse(beatmapInfo.WidescreenStoryboard);
|
||||
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
|
||||
Assert.AreEqual(0, beatmapInfo.CountdownOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,111 +666,111 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
// Multi-segment
|
||||
var first = ((IHasPath)decoded.HitObjects[0]).Path;
|
||||
|
||||
Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
|
||||
Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
|
||||
Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
|
||||
Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
|
||||
Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null));
|
||||
|
||||
Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
|
||||
Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15)));
|
||||
Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132)));
|
||||
Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107)));
|
||||
Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(first.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
|
||||
Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(first.ControlPoints[3].Position, Is.EqualTo(new Vector2(68, 15)));
|
||||
Assert.That(first.ControlPoints[3].Type, Is.EqualTo(null));
|
||||
Assert.That(first.ControlPoints[4].Position, Is.EqualTo(new Vector2(259, -132)));
|
||||
Assert.That(first.ControlPoints[4].Type, Is.EqualTo(null));
|
||||
Assert.That(first.ControlPoints[5].Position, Is.EqualTo(new Vector2(92, -107)));
|
||||
Assert.That(first.ControlPoints[5].Type, Is.EqualTo(null));
|
||||
|
||||
// Single-segment
|
||||
var second = ((IHasPath)decoded.HitObjects[1]).Path;
|
||||
|
||||
Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
|
||||
Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
|
||||
Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
|
||||
Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
|
||||
Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
|
||||
Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null));
|
||||
Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
|
||||
Assert.That(second.ControlPoints[2].Type, Is.EqualTo(null));
|
||||
|
||||
// Implicit multi-segment
|
||||
var third = ((IHasPath)decoded.HitObjects[2]).Path;
|
||||
|
||||
Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192)));
|
||||
Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192)));
|
||||
Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(third.ControlPoints[1].Position, Is.EqualTo(new Vector2(0, 192)));
|
||||
Assert.That(third.ControlPoints[1].Type, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[2].Position, Is.EqualTo(new Vector2(224, 192)));
|
||||
Assert.That(third.ControlPoints[2].Type, Is.EqualTo(null));
|
||||
|
||||
Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0)));
|
||||
Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192)));
|
||||
Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192)));
|
||||
Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
|
||||
Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[3].Position, Is.EqualTo(new Vector2(224, 0)));
|
||||
Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(third.ControlPoints[4].Position, Is.EqualTo(new Vector2(224, -192)));
|
||||
Assert.That(third.ControlPoints[4].Type, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[5].Position, Is.EqualTo(new Vector2(480, -192)));
|
||||
Assert.That(third.ControlPoints[5].Type, Is.EqualTo(null));
|
||||
Assert.That(third.ControlPoints[6].Position, Is.EqualTo(new Vector2(480, 0)));
|
||||
Assert.That(third.ControlPoints[6].Type, Is.EqualTo(null));
|
||||
|
||||
// Last control point duplicated
|
||||
var fourth = ((IHasPath)decoded.HitObjects[3]).Path;
|
||||
|
||||
Assert.That(fourth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(fourth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(fourth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1)));
|
||||
Assert.That(fourth.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fourth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2)));
|
||||
Assert.That(fourth.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fourth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fourth.ControlPoints[3].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fourth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fourth.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fourth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(fourth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
|
||||
Assert.That(fourth.ControlPoints[1].Type, Is.EqualTo(null));
|
||||
Assert.That(fourth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
|
||||
Assert.That(fourth.ControlPoints[2].Type, Is.EqualTo(null));
|
||||
Assert.That(fourth.ControlPoints[3].Position, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fourth.ControlPoints[3].Type, Is.EqualTo(null));
|
||||
Assert.That(fourth.ControlPoints[4].Position, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fourth.ControlPoints[4].Type, Is.EqualTo(null));
|
||||
|
||||
// Last control point in segment duplicated
|
||||
var fifth = ((IHasPath)decoded.HitObjects[4]).Path;
|
||||
|
||||
Assert.That(fifth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(fifth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(fifth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1)));
|
||||
Assert.That(fifth.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2)));
|
||||
Assert.That(fifth.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fifth.ControlPoints[3].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fifth.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(fifth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
|
||||
Assert.That(fifth.ControlPoints[1].Type, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
|
||||
Assert.That(fifth.ControlPoints[2].Type, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[3].Position, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fifth.ControlPoints[3].Type, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[4].Position, Is.EqualTo(new Vector2(3, 3)));
|
||||
Assert.That(fifth.ControlPoints[4].Type, Is.EqualTo(null));
|
||||
|
||||
Assert.That(fifth.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(4, 4)));
|
||||
Assert.That(fifth.ControlPoints[5].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(fifth.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(5, 5)));
|
||||
Assert.That(fifth.ControlPoints[6].Type.Value, Is.EqualTo(null));
|
||||
Assert.That(fifth.ControlPoints[5].Position, Is.EqualTo(new Vector2(4, 4)));
|
||||
Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.Bezier));
|
||||
Assert.That(fifth.ControlPoints[6].Position, Is.EqualTo(new Vector2(5, 5)));
|
||||
Assert.That(fifth.ControlPoints[6].Type, Is.EqualTo(null));
|
||||
|
||||
// Implicit perfect-curve multi-segment(Should convert to bezier to match stable)
|
||||
var sixth = ((IHasPath)decoded.HitObjects[5]).Path;
|
||||
|
||||
Assert.That(sixth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(sixth.ControlPoints[0].Type.Value == PathType.Bezier);
|
||||
Assert.That(sixth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145)));
|
||||
Assert.That(sixth.ControlPoints[1].Type.Value == null);
|
||||
Assert.That(sixth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75)));
|
||||
Assert.That(sixth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(sixth.ControlPoints[0].Type == PathType.Bezier);
|
||||
Assert.That(sixth.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
|
||||
Assert.That(sixth.ControlPoints[1].Type == null);
|
||||
Assert.That(sixth.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
|
||||
|
||||
Assert.That(sixth.ControlPoints[2].Type.Value == PathType.Bezier);
|
||||
Assert.That(sixth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145)));
|
||||
Assert.That(sixth.ControlPoints[3].Type.Value == null);
|
||||
Assert.That(sixth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20)));
|
||||
Assert.That(sixth.ControlPoints[4].Type.Value == null);
|
||||
Assert.That(sixth.ControlPoints[2].Type == PathType.Bezier);
|
||||
Assert.That(sixth.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
|
||||
Assert.That(sixth.ControlPoints[3].Type == null);
|
||||
Assert.That(sixth.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
|
||||
Assert.That(sixth.ControlPoints[4].Type == null);
|
||||
|
||||
// Explicit perfect-curve multi-segment(Should not convert to bezier)
|
||||
var seventh = ((IHasPath)decoded.HitObjects[6]).Path;
|
||||
|
||||
Assert.That(seventh.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(seventh.ControlPoints[0].Type.Value == PathType.PerfectCurve);
|
||||
Assert.That(seventh.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145)));
|
||||
Assert.That(seventh.ControlPoints[1].Type.Value == null);
|
||||
Assert.That(seventh.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75)));
|
||||
Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(seventh.ControlPoints[0].Type == PathType.PerfectCurve);
|
||||
Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
|
||||
Assert.That(seventh.ControlPoints[1].Type == null);
|
||||
Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
|
||||
|
||||
Assert.That(seventh.ControlPoints[2].Type.Value == PathType.PerfectCurve);
|
||||
Assert.That(seventh.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145)));
|
||||
Assert.That(seventh.ControlPoints[3].Type.Value == null);
|
||||
Assert.That(seventh.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20)));
|
||||
Assert.That(seventh.ControlPoints[4].Type.Value == null);
|
||||
Assert.That(seventh.ControlPoints[2].Type == PathType.PerfectCurve);
|
||||
Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
|
||||
Assert.That(seventh.ControlPoints[3].Type == null);
|
||||
Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
|
||||
Assert.That(seventh.ControlPoints[4].Type == null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||
|
||||
protected override ISkin GetSkin() => throw new NotImplementedException();
|
||||
protected internal override ISkin GetSkin() => throw new NotImplementedException();
|
||||
|
||||
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -50,12 +50,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var beatmap = decodeAsJson(normal);
|
||||
var beatmapInfo = beatmap.BeatmapInfo;
|
||||
Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
|
||||
Assert.AreEqual(false, beatmapInfo.Countdown);
|
||||
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
|
||||
Assert.AreEqual(false, beatmapInfo.SpecialStyle);
|
||||
Assert.IsTrue(beatmapInfo.RulesetID == 0);
|
||||
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks);
|
||||
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard);
|
||||
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
|
||||
Assert.AreEqual(0, beatmapInfo.CountdownOffset);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// 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 System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneBeatmapDifficultyCache : OsuTestScene
|
||||
{
|
||||
public const double BASE_STARS = 5.55;
|
||||
|
||||
private BeatmapSetInfo importedSet;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
private TestBeatmapDifficultyCache difficultyCache;
|
||||
|
||||
private IBindable<StarDifficulty?> starDifficultyBindable;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase osu)
|
||||
{
|
||||
importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).Result;
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup difficulty cache", () =>
|
||||
{
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
|
||||
Child = difficultyCache = new TestBeatmapDifficultyCache();
|
||||
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(importedSet.Beatmaps.First());
|
||||
});
|
||||
|
||||
AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStarDifficultyChangesOnModSettings()
|
||||
{
|
||||
OsuModDoubleTime dt = null;
|
||||
|
||||
AddStep("set computation function", () => difficultyCache.ComputeDifficulty = lookup =>
|
||||
{
|
||||
var modRateAdjust = (ModRateAdjust)lookup.OrderedMods.SingleOrDefault(mod => mod is ModRateAdjust);
|
||||
return new StarDifficulty(BASE_STARS + modRateAdjust?.SpeedChange.Value ?? 0, 0);
|
||||
});
|
||||
|
||||
AddStep("change selected mod to DT", () => SelectedMods.Value = new[] { dt = new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
|
||||
AddUntilStep($"star difficulty -> {BASE_STARS + 1.5}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.5);
|
||||
|
||||
AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 1.25);
|
||||
AddUntilStep($"star difficulty -> {BASE_STARS + 1.25}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.25);
|
||||
|
||||
AddStep("change selected mod to NC", () => SelectedMods.Value = new[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } });
|
||||
AddUntilStep($"star difficulty -> {BASE_STARS + 1.75}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.75);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStarDifficultyAdjustHashCodeConflict()
|
||||
{
|
||||
OsuModDifficultyAdjust difficultyAdjust = null;
|
||||
|
||||
AddStep("set computation function", () => difficultyCache.ComputeDifficulty = lookup =>
|
||||
{
|
||||
var modDifficultyAdjust = (ModDifficultyAdjust)lookup.OrderedMods.SingleOrDefault(mod => mod is ModDifficultyAdjust);
|
||||
return new StarDifficulty(BASE_STARS * (modDifficultyAdjust?.OverallDifficulty.Value ?? 1), 0);
|
||||
});
|
||||
|
||||
AddStep("change selected mod to DA", () => SelectedMods.Value = new[] { difficultyAdjust = new OsuModDifficultyAdjust() });
|
||||
AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS);
|
||||
|
||||
AddStep("change DA difficulty to 0.5", () => difficultyAdjust.OverallDifficulty.Value = 0.5f);
|
||||
AddUntilStep($"star difficulty -> {BASE_STARS * 0.5f}", () => starDifficultyBindable.Value?.Stars == BASE_STARS / 2);
|
||||
|
||||
// hash code of 0 (the value) conflicts with the hash code of null (the initial/default value).
|
||||
// it's important that the mod reference and its underlying bindable references stay the same to demonstrate this failure.
|
||||
AddStep("change DA difficulty to 0", () => difficultyAdjust.OverallDifficulty.Value = 0);
|
||||
AddUntilStep("star difficulty -> 0", () => starDifficultyBindable.Value?.Stars == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyEqualsWithDifferentModInstances()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyEqualsWithDifferentModOrder()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyDoesntEqualWithDifferentModSettings()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
|
||||
|
||||
Assert.That(key1, Is.Not.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyEqualWithMatchingModSettings()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
|
||||
}
|
||||
|
||||
[TestCase(1.3, DifficultyRating.Easy)]
|
||||
[TestCase(1.993, DifficultyRating.Easy)]
|
||||
[TestCase(1.998, DifficultyRating.Normal)]
|
||||
[TestCase(2.4, DifficultyRating.Normal)]
|
||||
[TestCase(2.693, DifficultyRating.Normal)]
|
||||
[TestCase(2.698, DifficultyRating.Hard)]
|
||||
[TestCase(3.5, DifficultyRating.Hard)]
|
||||
[TestCase(3.993, DifficultyRating.Hard)]
|
||||
[TestCase(3.997, DifficultyRating.Insane)]
|
||||
[TestCase(5.0, DifficultyRating.Insane)]
|
||||
[TestCase(5.292, DifficultyRating.Insane)]
|
||||
[TestCase(5.297, DifficultyRating.Expert)]
|
||||
[TestCase(6.2, DifficultyRating.Expert)]
|
||||
[TestCase(6.493, DifficultyRating.Expert)]
|
||||
[TestCase(6.498, DifficultyRating.ExpertPlus)]
|
||||
[TestCase(8.3, DifficultyRating.ExpertPlus)]
|
||||
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
|
||||
{
|
||||
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
|
||||
|
||||
Assert.AreEqual(expectedBracket, actualBracket);
|
||||
}
|
||||
|
||||
private class TestBeatmapDifficultyCache : BeatmapDifficultyCache
|
||||
{
|
||||
public Func<DifficultyCacheLookup, StarDifficulty> ComputeDifficulty { get; set; }
|
||||
|
||||
protected override Task<StarDifficulty> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(ComputeDifficulty?.Invoke(lookup) ?? new StarDifficulty(BASE_STARS, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -19,6 +20,7 @@ namespace osu.Game.Tests.Chat
|
||||
{
|
||||
private ChannelManager channelManager;
|
||||
private int currentMessageId;
|
||||
private List<Message> sentMessages;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
@@ -34,6 +36,7 @@ namespace osu.Game.Tests.Chat
|
||||
AddStep("register request handling", () =>
|
||||
{
|
||||
currentMessageId = 0;
|
||||
sentMessages = new List<Message>();
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
@@ -44,16 +47,11 @@ namespace osu.Game.Tests.Chat
|
||||
return true;
|
||||
|
||||
case PostMessageRequest postMessage:
|
||||
postMessage.TriggerSuccess(new Message(++currentMessageId)
|
||||
{
|
||||
IsAction = postMessage.Message.IsAction,
|
||||
ChannelId = postMessage.Message.ChannelId,
|
||||
Content = postMessage.Message.Content,
|
||||
Links = postMessage.Message.Links,
|
||||
Timestamp = postMessage.Message.Timestamp,
|
||||
Sender = postMessage.Message.Sender
|
||||
});
|
||||
handlePostMessageRequest(postMessage);
|
||||
return true;
|
||||
|
||||
case MarkChannelAsReadRequest markRead:
|
||||
handleMarkChannelAsReadRequest(markRead);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -83,12 +81,65 @@ namespace osu.Game.Tests.Chat
|
||||
AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMarkAsReadIgnoringLocalMessages()
|
||||
{
|
||||
Channel channel = null;
|
||||
|
||||
AddStep("join channel and select it", () =>
|
||||
{
|
||||
channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
});
|
||||
|
||||
AddStep("post message", () => channelManager.PostMessage("Something interesting"));
|
||||
|
||||
AddStep("post /help command", () => channelManager.PostCommand("help", channel));
|
||||
AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
|
||||
AddStep("post /join command with no channel", () => channelManager.PostCommand("join", channel));
|
||||
AddStep("post /join command with non-existent channel", () => channelManager.PostCommand("join i-dont-exist", channel));
|
||||
AddStep("post non-existent command", () => channelManager.PostCommand("non-existent-cmd arg", channel));
|
||||
|
||||
AddStep("mark channel as read", () => channelManager.MarkChannelAsRead(channel));
|
||||
AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
|
||||
}
|
||||
|
||||
private void handlePostMessageRequest(PostMessageRequest request)
|
||||
{
|
||||
var message = new Message(++currentMessageId)
|
||||
{
|
||||
IsAction = request.Message.IsAction,
|
||||
ChannelId = request.Message.ChannelId,
|
||||
Content = request.Message.Content,
|
||||
Links = request.Message.Links,
|
||||
Timestamp = request.Message.Timestamp,
|
||||
Sender = request.Message.Sender
|
||||
};
|
||||
|
||||
sentMessages.Add(message);
|
||||
request.TriggerSuccess(message);
|
||||
}
|
||||
|
||||
private void handleMarkChannelAsReadRequest(MarkChannelAsReadRequest request)
|
||||
{
|
||||
// only accept messages that were sent through the API
|
||||
if (sentMessages.Contains(request.Message))
|
||||
{
|
||||
request.TriggerSuccess();
|
||||
}
|
||||
else
|
||||
{
|
||||
request.TriggerFailure(new APIException("unknown message!", null));
|
||||
}
|
||||
}
|
||||
|
||||
private Channel createChannel(int id, ChannelType type) => new Channel(new User())
|
||||
{
|
||||
Id = id,
|
||||
Name = $"Channel {id}",
|
||||
Topic = $"Topic of channel {id} with type {type}",
|
||||
Type = type,
|
||||
LastMessageId = 0,
|
||||
};
|
||||
|
||||
private class ChannelManagerContainer : CompositeDrawable
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Collections.IO
|
||||
@@ -127,7 +128,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
[Test]
|
||||
public async Task TestSaveAndReload()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload", bypassCleanup: true))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -148,7 +149,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
}
|
||||
}
|
||||
|
||||
using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload"))
|
||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload"))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Gameplay
|
||||
@@ -121,6 +123,18 @@ namespace osu.Game.Tests.Gameplay
|
||||
AddAssert("Drawable lifetime is restored", () => dho.LifetimeStart == 666 && dho.LifetimeEnd == 999);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStateChangeBeforeLoadComplete()
|
||||
{
|
||||
TestDrawableHitObject dho = null;
|
||||
AddStep("Add DHO and apply result", () =>
|
||||
{
|
||||
Child = dho = new TestDrawableHitObject(new HitObject { StartTime = Time.Current });
|
||||
dho.MissForcefully();
|
||||
});
|
||||
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
|
||||
}
|
||||
|
||||
private class TestDrawableHitObject : DrawableHitObject
|
||||
{
|
||||
public const double INITIAL_LIFETIME_OFFSET = 100;
|
||||
@@ -141,6 +155,19 @@ namespace osu.Game.Tests.Gameplay
|
||||
if (SetLifetimeStartOnApply)
|
||||
LifetimeStart = LIFETIME_ON_APPLY;
|
||||
}
|
||||
|
||||
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
if (state != ArmedState.Miss)
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
return;
|
||||
}
|
||||
|
||||
this.FadeOut(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestLifetimeEntry : HitObjectLifetimeEntry
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
protected override ISkin GetSkin() => new TestSkin("test-sample", resources);
|
||||
protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources);
|
||||
}
|
||||
|
||||
private class TestDrawableStoryboardSample : DrawableStoryboardSample
|
||||
|
||||
@@ -8,7 +8,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Tests.Visual.Navigation;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Input
|
||||
{
|
||||
|
||||
@@ -6,10 +6,10 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.IO;
|
||||
|
||||
@@ -278,7 +278,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
private static string getDefaultLocationFor(string testTypeName)
|
||||
{
|
||||
string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName);
|
||||
string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, testTypeName);
|
||||
|
||||
if (Directory.Exists(path))
|
||||
Directory.Delete(path, true);
|
||||
@@ -288,7 +288,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
private string prepareCustomPath(string suffix = "")
|
||||
{
|
||||
string path = Path.Combine(RuntimeInfo.StartupDirectory, $"custom-path{suffix}");
|
||||
string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path{suffix}");
|
||||
|
||||
if (Directory.Exists(path))
|
||||
Directory.Delete(path, true);
|
||||
@@ -308,6 +308,19 @@ namespace osu.Game.Tests.NonVisual
|
||||
InitialStorage = new DesktopStorage(defaultStorageLocation, this);
|
||||
InitialStorage.DeleteDirectory(string.Empty);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
try
|
||||
{
|
||||
// the storage may have changed from the initial location.
|
||||
// this handles cleanup of the initial location.
|
||||
InitialStorage.DeleteDirectory(string.Empty);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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.Globalization;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -20,7 +19,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
[TestCase(1, "100.00%")]
|
||||
public void TestAccuracyFormatting(double input, string expectedOutput)
|
||||
{
|
||||
Assert.AreEqual(expectedOutput, input.FormatAccuracy(CultureInfo.InvariantCulture));
|
||||
Assert.AreEqual(expectedOutput, input.FormatAccuracy().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
AddRepeatStep("add some users", () => Client.AddUser(new User { Id = id++ }), 5);
|
||||
checkPlayingUserCount(0);
|
||||
|
||||
AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null);
|
||||
|
||||
changeState(3, MultiplayerUserState.WaitingForLoad);
|
||||
checkPlayingUserCount(3);
|
||||
|
||||
@@ -41,6 +43,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
|
||||
AddStep("leave room", () => Client.LeaveRoom());
|
||||
checkPlayingUserCount(0);
|
||||
|
||||
AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Extensions;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
[TestFixture]
|
||||
public class TimeDisplayExtensionTest
|
||||
{
|
||||
private static readonly object[][] editor_formatted_duration_tests =
|
||||
{
|
||||
new object[] { new TimeSpan(0, 0, 0, 0, 50), "00:00:050" },
|
||||
new object[] { new TimeSpan(0, 0, 0, 10, 50), "00:10:050" },
|
||||
new object[] { new TimeSpan(0, 0, 5, 10), "05:10:000" },
|
||||
new object[] { new TimeSpan(0, 1, 5, 10), "65:10:000" },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(editor_formatted_duration_tests))]
|
||||
public void TestEditorFormat(TimeSpan input, string expectedOutput)
|
||||
{
|
||||
Assert.AreEqual(expectedOutput, input.ToEditorFormattedString());
|
||||
}
|
||||
|
||||
private static readonly object[][] formatted_duration_tests =
|
||||
{
|
||||
new object[] { new TimeSpan(0, 0, 10), "00:10" },
|
||||
new object[] { new TimeSpan(0, 5, 10), "05:10" },
|
||||
new object[] { new TimeSpan(1, 5, 10), "01:05:10" },
|
||||
new object[] { new TimeSpan(1, 1, 5, 10), "01:01:05:10" },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(formatted_duration_tests))]
|
||||
public void TestFormattedDuration(TimeSpan input, string expectedOutput)
|
||||
{
|
||||
Assert.AreEqual(expectedOutput, input.ToFormattedDuration().ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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 MessagePack;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestMultiplayerMessagePackSerialization
|
||||
{
|
||||
[Test]
|
||||
public void TestSerialiseRoom()
|
||||
{
|
||||
var room = new MultiplayerRoom(1)
|
||||
{
|
||||
MatchState = new TeamVersusRoomState()
|
||||
};
|
||||
|
||||
var serialized = MessagePackSerializer.Serialize(room);
|
||||
|
||||
var deserialized = MessagePackSerializer.Deserialize<MultiplayerRoom>(serialized);
|
||||
|
||||
Assert.IsTrue(deserialized.MatchState is TeamVersusRoomState);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSerialiseUserStateExpected()
|
||||
{
|
||||
var state = new TeamVersusUserState();
|
||||
|
||||
var serialized = MessagePackSerializer.Serialize(typeof(MatchUserState), state);
|
||||
var deserialized = MessagePackSerializer.Deserialize<MatchUserState>(serialized);
|
||||
|
||||
Assert.IsTrue(deserialized is TeamVersusUserState);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSerialiseUnionFailsWithSingalR()
|
||||
{
|
||||
var state = new TeamVersusUserState();
|
||||
|
||||
// SignalR serialises using the actual type, rather than a base specification.
|
||||
var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state);
|
||||
|
||||
// works with explicit type specified.
|
||||
MessagePackSerializer.Deserialize<TeamVersusUserState>(serialized);
|
||||
|
||||
// fails with base (union) type.
|
||||
Assert.Throws<MessagePackSerializationException>(() => MessagePackSerializer.Deserialize<MatchUserState>(serialized));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSerialiseUnionSucceedsWithWorkaround()
|
||||
{
|
||||
var state = new TeamVersusUserState();
|
||||
|
||||
// SignalR serialises using the actual type, rather than a base specification.
|
||||
var serialized = MessagePackSerializer.Serialize(typeof(TeamVersusUserState), state, SignalRUnionWorkaroundResolver.OPTIONS);
|
||||
|
||||
// works with explicit type specified.
|
||||
MessagePackSerializer.Deserialize<TeamVersusUserState>(serialized);
|
||||
|
||||
// works with custom resolver.
|
||||
var deserialized = MessagePackSerializer.Deserialize<MatchUserState>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
|
||||
Assert.IsTrue(deserialized is TeamVersusUserState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,8 +168,8 @@ namespace osu.Game.Tests.Online
|
||||
|
||||
public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AllowImport.Task;
|
||||
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken));
|
||||
await AllowImport.Task.ConfigureAwait(false);
|
||||
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user