mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 12:17:26 +08:00
Merge branch 'master' into display-modes
This commit is contained in:
commit
9477a5373c
@ -5,6 +5,6 @@
|
||||
"version": "3.1.100"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "2.0.32"
|
||||
"Microsoft.Build.Traversal": "2.0.34"
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.403.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.421.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
<PackageReference Include="nunit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
</ItemGroup>
|
||||
|
21
osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs
Normal file
21
osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public abstract class CatchSkinnableTestScene : SkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(CatchRuleset),
|
||||
typeof(CatchLegacySkinTransformer),
|
||||
};
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset();
|
||||
}
|
||||
}
|
@ -4,21 +4,21 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneCatcher : SkinnableTestScene
|
||||
public class TestSceneCatcher : CatchSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||
{
|
||||
typeof(CatcherArea),
|
||||
typeof(CatcherSprite)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
@ -17,12 +17,11 @@ using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneCatcherArea : SkinnableTestScene
|
||||
public class TestSceneCatcherArea : CatchSkinnableTestScene
|
||||
{
|
||||
private RulesetInfo catchRuleset;
|
||||
|
||||
|
@ -3,20 +3,20 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneFruitObjects : SkinnableTestScene
|
||||
public class TestSceneFruitObjects : CatchSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||
{
|
||||
typeof(CatchHitObject),
|
||||
typeof(Fruit),
|
||||
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
typeof(DrawableBanana),
|
||||
typeof(DrawableBananaShower),
|
||||
typeof(Pulp),
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
132
osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
Normal file
132
osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
Normal file
@ -0,0 +1,132 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class TestSceneHyperDashColouring : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestDefaultFruitColour()
|
||||
{
|
||||
var skin = new TestSkin();
|
||||
|
||||
checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomFruitColour()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashFruitColour = Color4.Cyan
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomFruitColourPriority()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashColour = Color4.Goldenrod,
|
||||
HyperDashFruitColour = Color4.Cyan
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFruitColourFallback()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashColour = Color4.Goldenrod
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
||||
}
|
||||
|
||||
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
|
||||
{
|
||||
DrawableFruit drawableFruit = null;
|
||||
|
||||
AddStep("create hyper-dash fruit", () =>
|
||||
{
|
||||
var fruit = new Fruit { HyperDashTarget = new Banana() };
|
||||
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(4f),
|
||||
}, skin);
|
||||
});
|
||||
|
||||
AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
|
||||
}
|
||||
|
||||
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
|
||||
{
|
||||
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
|
||||
var testSkinProvider = new SkinProvidingContainer(skin);
|
||||
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
|
||||
|
||||
return legacySkinProvider
|
||||
.WithChild(testSkinProvider
|
||||
.WithChild(legacySkinTransformer
|
||||
.WithChild(child)));
|
||||
}
|
||||
|
||||
private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
|
||||
fruit.ChildrenOfType<SkinnableDrawable>().First().Drawable.ChildrenOfType<Sprite>().Any(c => c.Colour == expectedColour);
|
||||
|
||||
private class TestSkin : LegacySkin
|
||||
{
|
||||
public Color4 HyperDashColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
|
||||
}
|
||||
|
||||
public Color4 HyperDashAfterImageColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
|
||||
}
|
||||
|
||||
public Color4 HyperDashFruitColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
|
||||
}
|
||||
|
||||
public TestSkin()
|
||||
: base(new SkinInfo(), null, null, string.Empty)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -71,8 +71,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
||||
{
|
||||
using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
|
||||
halfCatcherWidth = catcher.CatchWidth * 0.5f;
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
@ -9,17 +9,26 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>
|
||||
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"Use the mouse to control the catcher.";
|
||||
|
||||
private DrawableRuleset<CatchHitObject> drawableRuleset;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
if (!drawableRuleset.HasReplayLoaded.Value)
|
||||
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
|
||||
}
|
||||
|
||||
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
|
||||
|
@ -70,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
|
||||
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
|
||||
|
||||
protected override float SamplePlaybackPosition => HitObject.X;
|
||||
|
||||
protected DrawableCatchHitObject(CatchHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
BorderColour = Color4.Red,
|
||||
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
BorderThickness = 12f * RADIUS_ADJUST,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
Alpha = 0.3f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Red,
|
||||
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -65,6 +65,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
|
||||
public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source.GetConfig<TLookup, TValue>(lookup);
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case CatchSkinColour colour:
|
||||
return source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
}
|
||||
|
||||
return source.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
Normal file
23
osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// 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.Skinning
|
||||
{
|
||||
public enum CatchSkinColour
|
||||
{
|
||||
/// <summary>
|
||||
/// The colour to be used for the catcher while in hyper-dashing state.
|
||||
/// </summary>
|
||||
HyperDash,
|
||||
|
||||
/// <summary>
|
||||
/// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
|
||||
/// </summary>
|
||||
HyperDashFruit,
|
||||
|
||||
/// <summary>
|
||||
/// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing.
|
||||
/// </summary>
|
||||
HyperDashAfterImage,
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -55,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
var hyperDash = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture(lookupName),
|
||||
Colour = Color4.Red,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Depth = 1,
|
||||
Alpha = 0.7f,
|
||||
Scale = new Vector2(1.2f)
|
||||
Scale = new Vector2(1.2f),
|
||||
Texture = skin.GetTexture(lookupName),
|
||||
Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
|
||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
||||
Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
};
|
||||
|
||||
AddInternal(hyperDash);
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class Catcher : Container, IKeyBindingHandler<CatchAction>
|
||||
{
|
||||
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we are hyper-dashing or not.
|
||||
/// </summary>
|
||||
@ -42,11 +44,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private const float allowed_catch_range = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// Width of the area that can be used to attempt catches during gameplay.
|
||||
/// </summary>
|
||||
internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range;
|
||||
|
||||
protected bool Dashing
|
||||
{
|
||||
get => dashing;
|
||||
@ -77,6 +74,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;
|
||||
|
||||
private Container<DrawableHitObject> caughtFruit;
|
||||
|
||||
private CatcherSprite catcherIdle;
|
||||
@ -104,7 +106,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
Size = new Vector2(CatcherArea.CATCHER_SIZE);
|
||||
if (difficulty != null)
|
||||
Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
Scale = calculateScale(difficulty);
|
||||
|
||||
catchWidth = CalculateCatchWidth(Scale);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -137,6 +141,26 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
updateCatcher();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
||||
/// </summary>
|
||||
private static Vector2 calculateScale(BeatmapDifficulty difficulty)
|
||||
=> new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||
/// </summary>
|
||||
/// <param name="scale">The scale of the catcher.</param>
|
||||
internal static float CalculateCatchWidth(Vector2 scale)
|
||||
=> CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||
internal static float CalculateCatchWidth(BeatmapDifficulty difficulty)
|
||||
=> CalculateCatchWidth(calculateScale(difficulty));
|
||||
|
||||
/// <summary>
|
||||
/// Add a caught fruit to the catcher's stack.
|
||||
/// </summary>
|
||||
@ -175,7 +199,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <returns>Whether the catch is possible.</returns>
|
||||
public bool AttemptCatch(CatchHitObject fruit)
|
||||
{
|
||||
var halfCatchWidth = CatchWidth * 0.5f;
|
||||
var halfCatchWidth = catchWidth * 0.5f;
|
||||
|
||||
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
|
||||
var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
|
||||
|
51
osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
Normal file
51
osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaLegacyReplayTest
|
||||
{
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
|
||||
|
||||
var frame = new ManiaReplayFrame(0, actions);
|
||||
var legacyFrame = frame.ToLegacy(beatmap);
|
||||
|
||||
var decodedFrame = new ManiaReplayFrame();
|
||||
decodedFrame.FromLegacy(legacyFrame, beatmap);
|
||||
|
||||
Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
|
||||
}
|
||||
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
|
||||
beatmap.Stages.Add(new StageDefinition { Columns = 5 });
|
||||
|
||||
var frame = new ManiaReplayFrame(0, actions);
|
||||
var legacyFrame = frame.ToLegacy(beatmap);
|
||||
|
||||
var decodedFrame = new ManiaReplayFrame();
|
||||
decodedFrame.FromLegacy(legacyFrame, beatmap);
|
||||
|
||||
Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
|
||||
}
|
||||
}
|
||||
}
|
@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AccentColour = Color4.OrangeRed,
|
||||
Clock = new FramedClock(new StopwatchClock()), // No scroll
|
||||
});
|
||||
|
||||
AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
|
||||
}
|
||||
|
||||
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,6 @@
|
||||
[General]
|
||||
Version: 2.4
|
||||
|
||||
[Mania]
|
||||
Keys: 4
|
||||
ColumnLineWidth: 3,1,3,1,1
|
@ -1,12 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -24,6 +27,15 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
[Cached(Type = typeof(IScrollingInfo))]
|
||||
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
|
||||
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(ManiaRuleset),
|
||||
typeof(ManiaLegacySkinTransformer),
|
||||
typeof(ManiaSettingsSubsection)
|
||||
};
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset();
|
||||
|
||||
protected ManiaSkinnableTestScene()
|
||||
{
|
||||
scrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
|
||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
|
@ -10,11 +10,10 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
public class TestSceneDrawableJudgement : SkinnableTestScene
|
||||
public class TestSceneDrawableJudgement : ManiaSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
|
||||
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
|
||||
{
|
||||
Child = new ManiaStage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
|
||||
Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mania.UI.Components;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
public class TestSceneStageBackground : ManiaSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||
{
|
||||
typeof(DefaultStageBackground),
|
||||
typeof(LegacyStageBackground),
|
||||
}).ToList();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
public class TestSceneStageForeground : ManiaSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||
{
|
||||
typeof(LegacyStageForeground),
|
||||
}).ToList();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
typeof(Column),
|
||||
typeof(ColumnBackground),
|
||||
typeof(ColumnHitObjectArea)
|
||||
typeof(ColumnHitObjectArea),
|
||||
typeof(DefaultKeyArea),
|
||||
typeof(DefaultHitTarget)
|
||||
};
|
||||
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
|
@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private const double time_after_tail = 5250;
|
||||
|
||||
private List<JudgementResult> judgementResults;
|
||||
private bool allJudgedFired;
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
@ -283,20 +282,15 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
p.ScoreProcessor.AllJudged += () =>
|
||||
{
|
||||
if (currentPlayer == p) allJudgedFired = true;
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
allJudgedFired = false;
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for all judged", () => allJudgedFired);
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
|
@ -1,17 +1,59 @@
|
||||
// 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.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
this.ChildrenOfType<HitObjectContainer>().ForEach(c => c.Clear());
|
||||
|
||||
ResetPlacement();
|
||||
|
||||
((ScrollingTestContainer)HitObjectContainer).Direction = ScrollingDirection.Down;
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestPlaceBeforeCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time < 0", () => getNote().StartTime < 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceAfterCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single()));
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time > 0", () => getNote().StartTime > 0);
|
||||
}
|
||||
|
||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
private IReadOnlyList<Mod> mods { get; set; } = Array.Empty<Mod>();
|
||||
|
||||
private readonly List<ManiaStage> stages = new List<ManiaStage>();
|
||||
private readonly List<Stage> stages = new List<Stage>();
|
||||
|
||||
private FillFlowContainer<ScrollingTestContainer> fill;
|
||||
|
||||
@ -81,9 +81,9 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre));
|
||||
}
|
||||
|
||||
private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
|
||||
private bool notesInStageAreAnchored(Stage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
|
||||
|
||||
private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
|
||||
private bool barsInStageAreAnchored(Stage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
|
||||
|
||||
private void createNote()
|
||||
{
|
||||
@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
var specialAction = ManiaAction.Special1;
|
||||
|
||||
var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
|
||||
var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
|
||||
stages.Add(stage);
|
||||
|
||||
return new ScrollingTestContainer(direction)
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
||||
using osu.Game.Rulesets.Mania.MathUtils;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
||||
using osuTK;
|
||||
using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
TargetColumns = (int)Math.Max(1, roundedCircleSize);
|
||||
|
||||
if (TargetColumns >= 10)
|
||||
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
TargetColumns /= 2;
|
||||
Dual = true;
|
||||
@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject);
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
|
||||
{
|
||||
@ -239,8 +238,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTimeData.Duration,
|
||||
Column = column,
|
||||
Head = { Samples = sampleInfoListAt(HitObject.StartTime) },
|
||||
Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
});
|
||||
}
|
||||
else if (HitObject is IHasXPosition)
|
||||
@ -255,22 +254,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the sample info list at a point in time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to retrieve the sample info list from.</param>
|
||||
/// <returns></returns>
|
||||
private IList<HitSampleInfo> sampleInfoListAt(double time)
|
||||
{
|
||||
if (!(HitObject is IHasCurve curveData))
|
||||
return HitObject.Samples;
|
||||
|
||||
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
|
||||
|
||||
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
|
||||
return curveData.NodeSamples[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -505,16 +505,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
}
|
||||
else
|
||||
{
|
||||
var holdNote = new HoldNote
|
||||
newObject = new HoldNote
|
||||
{
|
||||
StartTime = startTime,
|
||||
Column = column,
|
||||
Duration = endTime - startTime,
|
||||
Head = { Samples = sampleInfoListAt(startTime) },
|
||||
Tail = { Samples = sampleInfoListAt(endTime) }
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
};
|
||||
|
||||
newObject = holdNote;
|
||||
}
|
||||
|
||||
pattern.Add(newObject);
|
||||
|
@ -64,21 +64,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
if (holdNote)
|
||||
{
|
||||
var hold = new HoldNote
|
||||
newObject = new HoldNote
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTime - HitObject.StartTime,
|
||||
Column = column,
|
||||
Duration = endTime - HitObject.StartTime
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
};
|
||||
|
||||
if (hold.Head.Samples == null)
|
||||
hold.Head.Samples = new List<HitSampleInfo>();
|
||||
|
||||
hold.Head.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
|
||||
|
||||
hold.Tail.Samples = HitObject.Samples;
|
||||
|
||||
newObject = hold;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
64
osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs
Normal file
64
osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs
Normal file
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class DualStageVariantGenerator
|
||||
{
|
||||
private readonly int singleStageVariant;
|
||||
private readonly InputKey[] stage1LeftKeys;
|
||||
private readonly InputKey[] stage1RightKeys;
|
||||
private readonly InputKey[] stage2LeftKeys;
|
||||
private readonly InputKey[] stage2RightKeys;
|
||||
|
||||
public DualStageVariantGenerator(int singleStageVariant)
|
||||
{
|
||||
this.singleStageVariant = singleStageVariant;
|
||||
|
||||
// 10K is special because it expands towards the centre of the keyboard (VM/BN), rather than towards the edges of the keyboard.
|
||||
if (singleStageVariant == 10)
|
||||
{
|
||||
stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.V };
|
||||
stage1RightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
|
||||
|
||||
stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B };
|
||||
stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
|
||||
}
|
||||
else
|
||||
{
|
||||
stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R };
|
||||
stage1RightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
|
||||
|
||||
stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G };
|
||||
stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<KeyBinding> GenerateMappings()
|
||||
{
|
||||
var stage1Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = stage1LeftKeys,
|
||||
RightKeys = stage1RightKeys,
|
||||
SpecialKey = InputKey.V,
|
||||
SpecialAction = ManiaAction.Special1,
|
||||
NormalActionStart = ManiaAction.Key1
|
||||
}.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
|
||||
|
||||
var stage2Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = stage2LeftKeys,
|
||||
RightKeys = stage2RightKeys,
|
||||
SpecialKey = InputKey.B,
|
||||
SpecialAction = ManiaAction.Special2,
|
||||
NormalActionStart = nextNormal
|
||||
}.GenerateKeyBindingsFor(singleStageVariant, out _);
|
||||
|
||||
return stage1Bindings.Concat(stage2Bindings);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osuTK;
|
||||
@ -46,6 +47,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
bodyPiece.Height = (bottomPosition - topPosition).Y;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
EndPlacement(true);
|
||||
}
|
||||
|
||||
private double originalStartTime;
|
||||
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition)
|
||||
|
@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 1,
|
||||
BorderColour = colours.Yellow,
|
||||
Child = new Box
|
||||
|
@ -50,16 +50,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
|
||||
HitObject.Column = Column.Index;
|
||||
BeginPlacement(TimeAt(e.ScreenSpaceMousePosition));
|
||||
BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
EndPlacement(true);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
if (!PlacementActive)
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
|
||||
@ -26,5 +27,15 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
Width = SnappedWidth;
|
||||
Position = SnappedMousePosition;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
base.OnMouseDown(e);
|
||||
|
||||
// Place the note immediately.
|
||||
EndPlacement(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,5 +78,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
[Description("Key 18")]
|
||||
Key18,
|
||||
|
||||
[Description("Key 19")]
|
||||
Key19,
|
||||
|
||||
[Description("Key 20")]
|
||||
Key20,
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaRuleset : Ruleset, ILegacyRuleset
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of supported keys in a single stage.
|
||||
/// </summary>
|
||||
public const int MAX_STAGE_KEYS = 10;
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||
@ -202,6 +207,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ManiaModKey7(),
|
||||
new ManiaModKey8(),
|
||||
new ManiaModKey9(),
|
||||
new ManiaModKey10(),
|
||||
new ManiaModKey1(),
|
||||
new ManiaModKey2(),
|
||||
new ManiaModKey3()),
|
||||
@ -250,9 +256,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 1; i <= 9; i++)
|
||||
for (int i = 1; i <= MAX_STAGE_KEYS; i++)
|
||||
yield return (int)PlayfieldType.Single + i;
|
||||
for (int i = 2; i <= 18; i += 2)
|
||||
for (int i = 2; i <= MAX_STAGE_KEYS * 2; i += 2)
|
||||
yield return (int)PlayfieldType.Dual + i;
|
||||
}
|
||||
}
|
||||
@ -262,73 +268,10 @@ namespace osu.Game.Rulesets.Mania
|
||||
switch (getPlayfieldType(variant))
|
||||
{
|
||||
case PlayfieldType.Single:
|
||||
return new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = new[]
|
||||
{
|
||||
InputKey.A,
|
||||
InputKey.S,
|
||||
InputKey.D,
|
||||
InputKey.F
|
||||
},
|
||||
RightKeys = new[]
|
||||
{
|
||||
InputKey.J,
|
||||
InputKey.K,
|
||||
InputKey.L,
|
||||
InputKey.Semicolon
|
||||
},
|
||||
SpecialKey = InputKey.Space,
|
||||
SpecialAction = ManiaAction.Special1,
|
||||
NormalActionStart = ManiaAction.Key1,
|
||||
}.GenerateKeyBindingsFor(variant, out _);
|
||||
return new SingleStageVariantGenerator(variant).GenerateMappings();
|
||||
|
||||
case PlayfieldType.Dual:
|
||||
int keys = getDualStageKeyCount(variant);
|
||||
|
||||
var stage1Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = new[]
|
||||
{
|
||||
InputKey.Q,
|
||||
InputKey.W,
|
||||
InputKey.E,
|
||||
InputKey.R,
|
||||
},
|
||||
RightKeys = new[]
|
||||
{
|
||||
InputKey.X,
|
||||
InputKey.C,
|
||||
InputKey.V,
|
||||
InputKey.B
|
||||
},
|
||||
SpecialKey = InputKey.S,
|
||||
SpecialAction = ManiaAction.Special1,
|
||||
NormalActionStart = ManiaAction.Key1
|
||||
}.GenerateKeyBindingsFor(keys, out var nextNormal);
|
||||
|
||||
var stage2Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = new[]
|
||||
{
|
||||
InputKey.Number7,
|
||||
InputKey.Number8,
|
||||
InputKey.Number9,
|
||||
InputKey.Number0
|
||||
},
|
||||
RightKeys = new[]
|
||||
{
|
||||
InputKey.K,
|
||||
InputKey.L,
|
||||
InputKey.Semicolon,
|
||||
InputKey.Quote
|
||||
},
|
||||
SpecialKey = InputKey.I,
|
||||
SpecialAction = ManiaAction.Special2,
|
||||
NormalActionStart = nextNormal
|
||||
}.GenerateKeyBindingsFor(keys, out _);
|
||||
|
||||
return stage1Bindings.Concat(stage2Bindings);
|
||||
return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings();
|
||||
}
|
||||
|
||||
return Array.Empty<KeyBinding>();
|
||||
@ -364,59 +307,6 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
|
||||
}
|
||||
|
||||
private class VariantMappingGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the left hand.
|
||||
/// </summary>
|
||||
public InputKey[] LeftKeys;
|
||||
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the right hand.
|
||||
/// </summary>
|
||||
public InputKey[] RightKeys;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InputKey"/> for the special key.
|
||||
/// </summary>
|
||||
public InputKey SpecialKey;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
|
||||
/// </summary>
|
||||
public ManiaAction NormalActionStart;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> for the special column.
|
||||
/// </summary>
|
||||
public ManiaAction SpecialAction;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
|
||||
/// </summary>
|
||||
/// <param name="columns">The number of columns that need to be bound.</param>
|
||||
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
|
||||
/// <returns>The keybindings.</returns>
|
||||
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
|
||||
{
|
||||
ManiaAction currentNormalAction = NormalActionStart;
|
||||
|
||||
var bindings = new List<KeyBinding>();
|
||||
|
||||
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
|
||||
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
|
||||
|
||||
if (columns % 2 == 1)
|
||||
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
|
||||
|
||||
for (int i = 0; i < columns / 2; i++)
|
||||
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
|
||||
|
||||
nextNormalAction = currentNormalAction;
|
||||
return bindings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
HoldNoteHead,
|
||||
HoldNoteTail,
|
||||
HoldNoteBody,
|
||||
HitExplosion
|
||||
HitExplosion,
|
||||
StageBackground,
|
||||
StageForeground,
|
||||
}
|
||||
}
|
||||
|
13
osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
Normal file
13
osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// 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.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey10 : ManiaKeyMod
|
||||
{
|
||||
public override int KeyCount => 10;
|
||||
public override string Name => "Ten Keys";
|
||||
public override string Acronym => "10K";
|
||||
public override string Description => @"Play with ten keys.";
|
||||
}
|
||||
}
|
@ -51,7 +51,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece())
|
||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
})
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
},
|
||||
@ -127,6 +130,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
public override void PlaySamples()
|
||||
{
|
||||
// Samples are played by the head/tail notes.
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
@ -24,6 +25,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private ManiaPlayfield playfield { get; set; }
|
||||
|
||||
protected override float SamplePlaybackPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
if (playfield == null)
|
||||
return base.SamplePlaybackPosition;
|
||||
|
||||
return (float)HitObject.Column / playfield.TotalColumns;
|
||||
}
|
||||
}
|
||||
|
||||
protected DrawableManiaHitObject(ManiaHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
|
@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
||||
|
||||
public DefaultBodyPiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Blending = BlendingParameters.Additive;
|
||||
|
||||
AddLayout(subtractionCache);
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -28,7 +30,9 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
set
|
||||
{
|
||||
duration = value;
|
||||
Tail.StartTime = EndTime;
|
||||
|
||||
if (Tail != null)
|
||||
Tail.StartTime = EndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,8 +42,12 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
set
|
||||
{
|
||||
base.StartTime = value;
|
||||
Head.StartTime = value;
|
||||
Tail.StartTime = EndTime;
|
||||
|
||||
if (Head != null)
|
||||
Head.StartTime = value;
|
||||
|
||||
if (Tail != null)
|
||||
Tail.StartTime = EndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,20 +57,26 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
set
|
||||
{
|
||||
base.Column = value;
|
||||
Head.Column = value;
|
||||
Tail.Column = value;
|
||||
|
||||
if (Head != null)
|
||||
Head.Column = value;
|
||||
|
||||
if (Tail != null)
|
||||
Tail.Column = value;
|
||||
}
|
||||
}
|
||||
|
||||
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The head note of the hold.
|
||||
/// </summary>
|
||||
public readonly Note Head = new Note();
|
||||
public Note Head { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tail note of the hold.
|
||||
/// </summary>
|
||||
public readonly TailNote Tail = new TailNote();
|
||||
public TailNote Tail { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between ticks of this hold.
|
||||
@ -83,8 +97,19 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
|
||||
createTicks();
|
||||
|
||||
AddNested(Head);
|
||||
AddNested(Tail);
|
||||
AddNested(Head = new Note
|
||||
{
|
||||
StartTime = StartTime,
|
||||
Column = Column,
|
||||
Samples = getNodeSamples(0),
|
||||
});
|
||||
|
||||
AddNested(Tail = new TailNote
|
||||
{
|
||||
StartTime = EndTime,
|
||||
Column = Column,
|
||||
Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
||||
});
|
||||
}
|
||||
|
||||
private void createTicks()
|
||||
@ -105,5 +130,8 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
public override Judgement CreateJudgement() => new IgnoreJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
|
||||
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Mania.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public abstract class ManiaHitObject : HitObject, IHasColumn
|
||||
public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
|
||||
{
|
||||
public readonly Bindable<int> ColumnBindable = new Bindable<int>();
|
||||
|
||||
@ -20,5 +21,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
||||
float IHasXPosition.X => Column;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
while (activeColumns > 0)
|
||||
{
|
||||
var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter);
|
||||
bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
|
||||
|
||||
if ((activeColumns & 1) > 0)
|
||||
Actions.Add(isSpecial ? specialAction : normalAction);
|
||||
@ -58,33 +58,87 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
int keys = 0;
|
||||
|
||||
var specialColumns = new List<int>();
|
||||
|
||||
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
|
||||
{
|
||||
if (maniaBeatmap.Stages.First().IsSpecialColumn(i))
|
||||
specialColumns.Add(i);
|
||||
}
|
||||
|
||||
foreach (var action in Actions)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ManiaAction.Special1:
|
||||
keys |= 1 << specialColumns[0];
|
||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
|
||||
break;
|
||||
|
||||
case ManiaAction.Special2:
|
||||
keys |= 1 << specialColumns[1];
|
||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
keys |= 1 << (action - ManiaAction.Key1);
|
||||
// the index in lazer, which doesn't include special keys.
|
||||
int nonSpecialKeyIndex = action - ManiaAction.Key1;
|
||||
|
||||
// the index inclusive of special keys.
|
||||
int overallIndex = 0;
|
||||
|
||||
// iterate to find the index including special keys.
|
||||
for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
|
||||
{
|
||||
// skip over special columns.
|
||||
if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
|
||||
continue;
|
||||
// found a non-special column to use.
|
||||
if (nonSpecialKeyIndex == 0)
|
||||
break;
|
||||
// found a non-special column but not ours.
|
||||
nonSpecialKeyIndex--;
|
||||
}
|
||||
|
||||
keys |= 1 << overallIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the overall index (across all stages) for a specified special key.
|
||||
/// </summary>
|
||||
/// <param name="maniaBeatmap">The beatmap.</param>
|
||||
/// <param name="specialOffset">The special key offset (0 is S1).</param>
|
||||
/// <returns>The overall index for the special column.</returns>
|
||||
private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
|
||||
{
|
||||
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
|
||||
{
|
||||
if (isColumnAtIndexSpecial(maniaBeatmap, i))
|
||||
{
|
||||
if (specialOffset == 0)
|
||||
return i;
|
||||
|
||||
specialOffset--;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the column at an overall index (across all stages) is a special column.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap.</param>
|
||||
/// <param name="index">The overall index to check.</param>
|
||||
private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
|
||||
{
|
||||
foreach (var stage in beatmap.Stages)
|
||||
{
|
||||
if (index >= stage.Columns)
|
||||
{
|
||||
index -= stage.Columns;
|
||||
continue;
|
||||
}
|
||||
|
||||
return stage.IsSpecialColumn(index);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Column index is too high.", nameof(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
41
osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs
Normal file
41
osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs
Normal file
@ -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.Collections.Generic;
|
||||
using osu.Framework.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class SingleStageVariantGenerator
|
||||
{
|
||||
private readonly int variant;
|
||||
private readonly InputKey[] leftKeys;
|
||||
private readonly InputKey[] rightKeys;
|
||||
|
||||
public SingleStageVariantGenerator(int variant)
|
||||
{
|
||||
this.variant = variant;
|
||||
|
||||
// 10K is special because it expands towards the centre of the keyboard (V/N), rather than towards the edges of the keyboard.
|
||||
if (variant == 10)
|
||||
{
|
||||
leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V };
|
||||
rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
|
||||
}
|
||||
else
|
||||
{
|
||||
leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F };
|
||||
rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<KeyBinding> GenerateMappings() => new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = leftKeys,
|
||||
RightKeys = rightKeys,
|
||||
SpecialKey = InputKey.Space,
|
||||
SpecialAction = ManiaAction.Special1,
|
||||
NormalActionStart = ManiaAction.Key1,
|
||||
}.GenerateKeyBindingsFor(variant, out _);
|
||||
}
|
||||
}
|
@ -50,17 +50,24 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
Color4 lineColour = GetManiaSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value
|
||||
?? Color4.White;
|
||||
|
||||
Color4 backgroundColour = GetManiaSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value
|
||||
?? Color4.Black;
|
||||
|
||||
Color4 lightColour = GetManiaSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
|
||||
?? Color4.White;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black
|
||||
Colour = backgroundColour
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = leftLineWidth,
|
||||
Scale = new Vector2(0.740f, 1),
|
||||
Colour = lineColour,
|
||||
Alpha = hasLeftLine ? 1 : 0
|
||||
},
|
||||
@ -70,6 +77,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = rightLineWidth,
|
||||
Scale = new Vector2(0.740f, 1),
|
||||
Colour = lineColour,
|
||||
Alpha = hasRightLine ? 1 : 0
|
||||
},
|
||||
@ -82,6 +90,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = lightColour,
|
||||
Texture = skin.GetTexture(lightImage),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
|
||||
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
|
||||
|
||||
explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true, frameLength: frameLength).With(d =>
|
||||
explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d =>
|
||||
{
|
||||
if (d == null)
|
||||
return;
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
bool showJudgementLine = GetManiaSkinConfig<bool>(skin, LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value
|
||||
?? true;
|
||||
|
||||
Color4 lineColour = GetManiaSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value
|
||||
?? Color4.White;
|
||||
|
||||
InternalChild = directionContainer = new Container
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
@ -52,6 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
Anchor = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1,
|
||||
Colour = lineColour,
|
||||
Alpha = showJudgementLine ? 0.9f : 0
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
private Container directionContainer;
|
||||
private Sprite noteSprite;
|
||||
|
||||
private float? minimumColumnWidth;
|
||||
|
||||
public LegacyNotePiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@ -29,6 +31,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||
{
|
||||
minimumColumnWidth = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
|
||||
|
||||
InternalChild = directionContainer = new Container
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
@ -47,8 +51,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
|
||||
if (noteSprite.Texture != null)
|
||||
{
|
||||
var scale = DrawWidth / noteSprite.Texture.DisplayWidth;
|
||||
noteSprite.Scale = new Vector2(scale);
|
||||
// The height is scaled to the minimum column width, if provided.
|
||||
float minimumWidth = minimumColumnWidth ?? DrawWidth;
|
||||
|
||||
noteSprite.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), noteSprite.Texture.DisplayWidth);
|
||||
}
|
||||
}
|
||||
|
||||
|
61
osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
Normal file
61
osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
Normal file
@ -0,0 +1,61 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
public class LegacyStageBackground : LegacyManiaElement
|
||||
{
|
||||
private Drawable leftSprite;
|
||||
private Drawable rightSprite;
|
||||
|
||||
public LegacyStageBackground()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
string leftImage = GetManiaSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
|
||||
?? "mania-stage-left";
|
||||
|
||||
string rightImage = GetManiaSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value
|
||||
?? "mania-stage-right";
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
leftSprite = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopRight,
|
||||
X = 0.05f,
|
||||
Texture = skin.GetTexture(leftImage),
|
||||
},
|
||||
rightSprite = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopLeft,
|
||||
X = -0.05f,
|
||||
Texture = skin.GetTexture(rightImage)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (leftSprite?.Height > 0)
|
||||
leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height);
|
||||
|
||||
if (rightSprite?.Height > 0)
|
||||
rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height);
|
||||
}
|
||||
}
|
||||
}
|
56
osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
Normal file
56
osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs
Normal file
@ -0,0 +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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
public class LegacyStageForeground : LegacyManiaElement
|
||||
{
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private Drawable sprite;
|
||||
|
||||
public LegacyStageForeground()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||
{
|
||||
string bottomImage = GetManiaSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
|
||||
?? "mania-stage-bottom";
|
||||
|
||||
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
|
||||
{
|
||||
if (d == null)
|
||||
return;
|
||||
|
||||
d.Scale = new Vector2(1.6f);
|
||||
});
|
||||
|
||||
if (sprite != null)
|
||||
InternalChild = sprite;
|
||||
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
if (sprite == null)
|
||||
return;
|
||||
|
||||
if (direction.NewValue == ScrollingDirection.Up)
|
||||
sprite.Anchor = sprite.Origin = Anchor.TopCentre;
|
||||
else
|
||||
sprite.Anchor = sprite.Origin = Anchor.BottomCentre;
|
||||
}
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
isLegacySkin = new Lazy<bool>(() => source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() => source.GetAnimation(
|
||||
source.GetConfig<ManiaSkinConfigurationLookup, string>(
|
||||
GetConfig<ManiaSkinConfigurationLookup, string>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value
|
||||
?? "mania-key1", true, true) != null);
|
||||
}
|
||||
@ -81,6 +81,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
|
||||
case ManiaSkinComponents.HitExplosion:
|
||||
return new LegacyHitExplosion();
|
||||
|
||||
case ManiaSkinComponents.StageBackground:
|
||||
return new LegacyStageBackground();
|
||||
|
||||
case ManiaSkinComponents.StageForeground:
|
||||
return new LegacyStageForeground();
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Index = index;
|
||||
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Width = COLUMN_WIDTH;
|
||||
|
||||
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground())
|
||||
{
|
||||
@ -138,6 +139,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
||||
=> DrawRectangle.Inflate(new Vector2(ManiaStage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
InternalChild = directionContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = ManiaStage.HIT_TARGET_POSITION,
|
||||
Height = Stage.HIT_TARGET_POSITION,
|
||||
Children = new[]
|
||||
{
|
||||
gradient = new Box
|
||||
@ -53,9 +53,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
keyIcon = new Container
|
||||
{
|
||||
Name = "Key icon",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(key_icon_size),
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
CornerRadius = key_icon_corner_radius,
|
||||
BorderThickness = 2,
|
||||
@ -88,11 +87,15 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
{
|
||||
if (direction.NewValue == ScrollingDirection.Up)
|
||||
{
|
||||
keyIcon.Anchor = Anchor.BottomCentre;
|
||||
keyIcon.Y = -20;
|
||||
directionContainer.Anchor = directionContainer.Origin = Anchor.TopLeft;
|
||||
gradient.Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
keyIcon.Anchor = Anchor.TopCentre;
|
||||
keyIcon.Y = 20;
|
||||
directionContainer.Anchor = directionContainer.Origin = Anchor.BottomLeft;
|
||||
gradient.Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black);
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
{
|
||||
public class DefaultStageBackground : CompositeDrawable
|
||||
{
|
||||
public DefaultStageBackground()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new Box
|
||||
{
|
||||
Name = "Background",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
{
|
||||
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
|
||||
?? ManiaStage.HIT_TARGET_POSITION;
|
||||
?? Stage.HIT_TARGET_POSITION;
|
||||
|
||||
Padding = Direction.Value == ScrollingDirection.Up
|
||||
? new MarginPadding { Top = hitPosition }
|
||||
|
@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
// Mania doesn't care about global velocity
|
||||
p.Velocity = 1;
|
||||
p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
|
||||
|
||||
// For non-mania beatmap, speed changes should only happen through timing points
|
||||
if (!isForCurrentRuleset)
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -14,9 +15,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
[Cached]
|
||||
public class ManiaPlayfield : ScrollingPlayfield
|
||||
{
|
||||
private readonly List<ManiaStage> stages = new List<ManiaStage>();
|
||||
private readonly List<Stage> stages = new List<Stage>();
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
|
||||
|
||||
@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
for (int i = 0; i < stageDefinitions.Count; i++)
|
||||
{
|
||||
var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
|
||||
var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
|
||||
|
||||
playfieldGrid.Content[0][i] = newStage;
|
||||
|
||||
@ -90,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// </summary>
|
||||
public int TotalColumns => stages.Sum(s => s.Columns.Count);
|
||||
|
||||
private ManiaStage getStageByColumn(int column)
|
||||
private Stage getStageByColumn(int column)
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
|
@ -6,7 +6,6 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@ -25,11 +24,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// A collection of <see cref="Column"/>s.
|
||||
/// </summary>
|
||||
public class ManiaStage : ScrollingPlayfield
|
||||
public class Stage : ScrollingPlayfield
|
||||
{
|
||||
public const float COLUMN_SPACING = 1;
|
||||
|
||||
public const float HIT_TARGET_POSITION = 50;
|
||||
public const float HIT_TARGET_POSITION = 110;
|
||||
|
||||
public IReadOnlyList<Column> Columns => columnFlow.Children;
|
||||
private readonly FillFlowContainer<Column> columnFlow;
|
||||
@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly int firstColumnIndex;
|
||||
|
||||
public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
|
||||
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
|
||||
{
|
||||
this.firstColumnIndex = firstColumnIndex;
|
||||
|
||||
@ -72,11 +71,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
AutoSizeAxes = Axes.X,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
|
||||
{
|
||||
Name = "Background",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
columnFlow = new FillFlowContainer<Column>
|
||||
{
|
||||
@ -103,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
}
|
||||
},
|
||||
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
judgements = new JudgementContainer<DrawableManiaJudgement>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
61
osu.Game.Rulesets.Mania/VariantMappingGenerator.cs
Normal file
61
osu.Game.Rulesets.Mania/VariantMappingGenerator.cs
Normal file
@ -0,0 +1,61 @@
|
||||
// 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 osu.Framework.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class VariantMappingGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the left hand.
|
||||
/// </summary>
|
||||
public InputKey[] LeftKeys;
|
||||
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the right hand.
|
||||
/// </summary>
|
||||
public InputKey[] RightKeys;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InputKey"/> for the special key.
|
||||
/// </summary>
|
||||
public InputKey SpecialKey;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
|
||||
/// </summary>
|
||||
public ManiaAction NormalActionStart;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> for the special column.
|
||||
/// </summary>
|
||||
public ManiaAction SpecialAction;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
|
||||
/// </summary>
|
||||
/// <param name="columns">The number of columns that need to be bound.</param>
|
||||
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
|
||||
/// <returns>The keybindings.</returns>
|
||||
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
|
||||
{
|
||||
ManiaAction currentNormalAction = NormalActionStart;
|
||||
|
||||
var bindings = new List<KeyBinding>();
|
||||
|
||||
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
|
||||
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
|
||||
|
||||
if (columns % 2 == 1)
|
||||
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
|
||||
|
||||
for (int i = 0; i < columns / 2; i++)
|
||||
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
|
||||
|
||||
nextNormalAction = currentNormalAction;
|
||||
return bindings;
|
||||
}
|
||||
}
|
||||
}
|
106
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
Normal file
106
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
Normal file
@ -0,0 +1,106 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public class TestSceneOsuModHidden : ModTestScene
|
||||
{
|
||||
public TestSceneOsuModHidden()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModHidden(),
|
||||
Autoplay = true,
|
||||
PassCondition = checkSomeHit
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModHidden(),
|
||||
Autoplay = true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
EndTime = 1000,
|
||||
},
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1200,
|
||||
EndTime = 2200,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 3200,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(384, 192),
|
||||
StartTime = 4200,
|
||||
}
|
||||
}
|
||||
},
|
||||
PassCondition = checkSomeHit
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModHidden(),
|
||||
Autoplay = true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
EndTime = 1000,
|
||||
},
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1200,
|
||||
EndTime = 2200,
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3200,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = 5200,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
|
||||
}
|
||||
}
|
||||
},
|
||||
PassCondition = checkSomeHit
|
||||
});
|
||||
|
||||
private bool checkSomeHit()
|
||||
{
|
||||
return Player.ScoreProcessor.JudgedHits >= 4;
|
||||
}
|
||||
}
|
||||
}
|
21
osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
Normal file
21
osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public abstract class OsuSkinnableTestScene : SkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(OsuRuleset),
|
||||
typeof(OsuLegacySkinTransformer),
|
||||
};
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
}
|
||||
}
|
@ -10,17 +10,16 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneDrawableJudgement : SkinnableTestScene
|
||||
public class TestSceneDrawableJudgement : OsuSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||
{
|
||||
typeof(DrawableJudgement),
|
||||
typeof(DrawableOsuJudgement)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
public TestSceneDrawableJudgement()
|
||||
{
|
||||
|
@ -3,26 +3,32 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneGameplayCursor : SkinnableTestScene
|
||||
public class TestSceneGameplayCursor : OsuSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
|
||||
{
|
||||
typeof(GameplayCursorContainer),
|
||||
typeof(OsuCursorContainer),
|
||||
typeof(OsuCursor),
|
||||
typeof(LegacyCursor),
|
||||
typeof(LegacyCursorTrail),
|
||||
typeof(CursorTrail)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
[Cached]
|
||||
private GameplayBeatmap gameplayBeatmap;
|
||||
|
@ -14,12 +14,11 @@ using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneHitCircle : SkinnableTestScene
|
||||
public class TestSceneHitCircle : OsuSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
|
447
osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
Normal file
447
osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
Normal file
@ -0,0 +1,447 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
|
||||
private const double late_miss_window = 500; // time after +500 is considered a miss
|
||||
|
||||
/// <summary>
|
||||
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestClickSecondCircleBeforeFirstCircleTime()
|
||||
{
|
||||
const double time_first_circle = 1500;
|
||||
const double time_second_circle = 1600;
|
||||
Vector2 positionFirstCircle = Vector2.Zero;
|
||||
Vector2 positionSecondCircle = new Vector2(80);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle
|
||||
},
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestClickSecondCircleAtFirstCircleTime()
|
||||
{
|
||||
const double time_first_circle = 1500;
|
||||
const double time_second_circle = 1600;
|
||||
Vector2 positionFirstCircle = Vector2.Zero;
|
||||
Vector2 positionSecondCircle = new Vector2(80);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle
|
||||
},
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestClickSecondCircleAfterFirstCircleTime()
|
||||
{
|
||||
const double time_first_circle = 1500;
|
||||
const double time_second_circle = 1600;
|
||||
Vector2 positionFirstCircle = Vector2.Zero;
|
||||
Vector2 positionSecondCircle = new Vector2(80);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle
|
||||
},
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[0], 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
|
||||
{
|
||||
const double time_first_circle = 1500;
|
||||
const double time_second_circle = 1600;
|
||||
Vector2 positionFirstCircle = Vector2.Zero;
|
||||
Vector2 positionSecondCircle = new Vector2(80);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle
|
||||
},
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
|
||||
addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestMissSliderHeadAndHitAllSliderTicks()
|
||||
{
|
||||
const double time_slider = 1500;
|
||||
const double time_circle = 1510;
|
||||
Vector2 positionCircle = Vector2.Zero;
|
||||
Vector2 positionSlider = new Vector2(80);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_circle,
|
||||
Position = positionCircle
|
||||
},
|
||||
new TestSlider
|
||||
{
|
||||
StartTime = time_slider,
|
||||
Position = positionSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests clicking hitting future slider ticks before a circle.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitSliderTicksBeforeCircle()
|
||||
{
|
||||
const double time_slider = 1500;
|
||||
const double time_circle = 1510;
|
||||
Vector2 positionCircle = Vector2.Zero;
|
||||
Vector2 positionSlider = new Vector2(80);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_circle,
|
||||
Position = positionCircle
|
||||
},
|
||||
new TestSlider
|
||||
{
|
||||
StartTime = time_slider,
|
||||
Position = positionSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
|
||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests clicking a future circle before a spinner.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitCircleBeforeSpinner()
|
||||
{
|
||||
const double time_spinner = 1500;
|
||||
const double time_circle = 1800;
|
||||
Vector2 positionCircle = Vector2.Zero;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestSpinner
|
||||
{
|
||||
StartTime = time_spinner,
|
||||
Position = new Vector2(256, 192),
|
||||
EndTime = time_spinner + 1000,
|
||||
},
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_circle,
|
||||
Position = positionCircle
|
||||
},
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitSliderHeadBeforeHitCircle()
|
||||
{
|
||||
const double time_circle = 1000;
|
||||
const double time_slider = 1200;
|
||||
Vector2 positionCircle = Vector2.Zero;
|
||||
Vector2 positionSlider = new Vector2(80);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestHitCircle
|
||||
{
|
||||
StartTime = time_circle,
|
||||
Position = positionCircle
|
||||
},
|
||||
new TestSlider
|
||||
{
|
||||
StartTime = time_slider,
|
||||
Position = positionSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
}
|
||||
|
||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||
{
|
||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
|
||||
}
|
||||
|
||||
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result)
|
||||
{
|
||||
AddAssert($"{name} judgement is {result}",
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
|
||||
}
|
||||
|
||||
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
||||
{
|
||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
||||
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
||||
}
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
private List<JudgementResult> judgementResults;
|
||||
|
||||
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = hitObjects,
|
||||
BeatmapInfo =
|
||||
{
|
||||
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private class TestHitCircle : HitCircle
|
||||
{
|
||||
protected override HitWindows CreateHitWindows() => new TestHitWindows();
|
||||
}
|
||||
|
||||
private class TestSlider : Slider
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DefaultsApplied += () =>
|
||||
{
|
||||
HeadCircle.HitWindows = new TestHitWindows();
|
||||
TailCircle.HitWindows = new TestHitWindows();
|
||||
|
||||
HeadCircle.HitWindows.SetDifficulty(0);
|
||||
TailCircle.HitWindows.SetDifficulty(0);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSpinner : Spinner
|
||||
{
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
SpinsRequired = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestHitWindows : HitWindows
|
||||
{
|
||||
private static readonly DifficultyRange[] ranges =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 500, 500, 500),
|
||||
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
|
||||
};
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => ranges;
|
||||
}
|
||||
|
||||
private class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, false, false)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestScenePathControlPointVisualiser : OsuTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(StringHumanizeExtensions),
|
||||
typeof(PathControlPointPiece),
|
||||
typeof(PathControlPointConnectionPiece)
|
||||
};
|
||||
|
||||
private Slider slider;
|
||||
private PathControlPointVisualiser visualiser;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
slider = new Slider();
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestAddOverlappingControlPoints()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200));
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
|
||||
AddAssert("last connection displayed", () =>
|
||||
{
|
||||
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300));
|
||||
return lastConnection.DrawWidth > 50;
|
||||
});
|
||||
}
|
||||
|
||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
|
||||
private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position)));
|
||||
}
|
||||
}
|
@ -22,12 +22,11 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneSlider : SkinnableTestScene
|
||||
public class TestSceneSlider : OsuSkinnableTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
|
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private const double time_slider_end = 4000;
|
||||
|
||||
private List<JudgementResult> judgementResults;
|
||||
private bool allJudgedFired;
|
||||
|
||||
/// <summary>
|
||||
/// Scenario:
|
||||
@ -375,20 +374,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
p.ScoreProcessor.AllJudged += () =>
|
||||
{
|
||||
if (currentPlayer == p) allJudgedFired = true;
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
allJudgedFired = false;
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for all judged", () => allJudgedFired);
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
|
@ -1,18 +1,302 @@
|
||||
// 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.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
HitObjectContainer.Clear();
|
||||
ResetPlacement();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBeginPlacementWithoutFinishing()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
assertPlaced(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceWithoutMovingMouse()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(0);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceWithMouseMovement()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 200));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(200);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceNormalControlPoint()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceTwoNormalControlPoints()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100, 100));
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceSegmentControlPoint()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertControlPointType(1, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMoveToPerfectCurveThenPlaceLinear()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertLength(100);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMoveToBezierThenPlacePerfectCurve()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMoveToFourthOrderBezierThenPlaceThirdOrderBezier()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400));
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceLinearSegmentThenPlaceLinearSegment()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertControlPointType(1, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceLinearSegmentThenPlacePerfectCurveSegment()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertControlPointType(1, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacePerfectCurveSegmentThenPlacePerfectCurveSegment()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(300, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 300));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(5);
|
||||
assertControlPointPosition(1, new Vector2(100, 0));
|
||||
assertControlPointPosition(2, new Vector2(100));
|
||||
assertControlPointPosition(3, new Vector2(200, 100));
|
||||
assertControlPointPosition(4, new Vector2(200));
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
assertControlPointType(2, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeginPlacementWithoutReleasingMouse()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
addMovementStep(new Vector2(400, 200));
|
||||
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(200);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||
|
||||
private void addClickStep(MouseButton button)
|
||||
{
|
||||
AddStep($"press {button}", () => InputManager.PressButton(button));
|
||||
AddStep($"release {button}", () => InputManager.ReleaseButton(button));
|
||||
}
|
||||
|
||||
private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected);
|
||||
|
||||
private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1));
|
||||
|
||||
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 assertControlPointPosition(int index, Vector2 position) =>
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1));
|
||||
|
||||
private Slider getSlider() => HitObjectContainer.Count > 0 ? (Slider)((DrawableSlider)HitObjectContainer[0]).HitObject : null;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
||||
}
|
||||
|
253
osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
Normal file
253
osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
Normal file
@ -0,0 +1,253 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneSliderSnaking : TestSceneOsuPlayer
|
||||
{
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; }
|
||||
|
||||
private TrackVirtualManual track;
|
||||
|
||||
protected override bool Autoplay => autoplay;
|
||||
private bool autoplay;
|
||||
|
||||
private readonly BindableBool snakingIn = new BindableBool();
|
||||
private readonly BindableBool snakingOut = new BindableBool();
|
||||
|
||||
private const double duration_of_span = 3605;
|
||||
private const double fade_in_modifier = -1200;
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
{
|
||||
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
track = (TrackVirtualManual)working.Track;
|
||||
return working;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetConfigCache configCache)
|
||||
{
|
||||
var config = (OsuRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
|
||||
config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn);
|
||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||
}
|
||||
|
||||
private DrawableSlider slider;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps() { }
|
||||
|
||||
[TestCase(0)]
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
public void TestSnakingEnabled(int sliderIndex)
|
||||
{
|
||||
AddStep("enable autoplay", () => autoplay = true);
|
||||
base.SetUpSteps();
|
||||
AddUntilStep("wait for track to start running", () => track.IsRunning);
|
||||
|
||||
double startTime = hitObjects[sliderIndex].StartTime;
|
||||
retrieveDrawableSlider(sliderIndex);
|
||||
setSnaking(true);
|
||||
|
||||
ensureSnakingIn(startTime + fade_in_modifier);
|
||||
|
||||
for (int i = 0; i < sliderIndex; i++)
|
||||
{
|
||||
// non-final repeats should not snake out
|
||||
ensureNoSnakingOut(startTime, i);
|
||||
}
|
||||
|
||||
// final repeat should snake out
|
||||
ensureSnakingOut(startTime, sliderIndex);
|
||||
}
|
||||
|
||||
[TestCase(0)]
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
public void TestSnakingDisabled(int sliderIndex)
|
||||
{
|
||||
AddStep("have autoplay", () => autoplay = true);
|
||||
base.SetUpSteps();
|
||||
AddUntilStep("wait for track to start running", () => track.IsRunning);
|
||||
|
||||
double startTime = hitObjects[sliderIndex].StartTime;
|
||||
retrieveDrawableSlider(sliderIndex);
|
||||
setSnaking(false);
|
||||
|
||||
ensureNoSnakingIn(startTime + fade_in_modifier);
|
||||
|
||||
for (int i = 0; i <= sliderIndex; i++)
|
||||
{
|
||||
// no snaking out ever, including final repeat
|
||||
ensureNoSnakingOut(startTime, i);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRepeatArrowDoesNotMoveWhenHit()
|
||||
{
|
||||
AddStep("enable autoplay", () => autoplay = true);
|
||||
setSnaking(true);
|
||||
base.SetUpSteps();
|
||||
|
||||
// repeat might have a chance to update its position depending on where in the frame its hit,
|
||||
// so some leniency is allowed here instead of checking strict equality
|
||||
checkPositionChange(16600, sliderRepeat, positionAlmostSame);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRepeatArrowMovesWhenNotHit()
|
||||
{
|
||||
AddStep("disable autoplay", () => autoplay = false);
|
||||
setSnaking(true);
|
||||
base.SetUpSteps();
|
||||
|
||||
checkPositionChange(16600, sliderRepeat, positionDecreased);
|
||||
}
|
||||
|
||||
private void retrieveDrawableSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () =>
|
||||
{
|
||||
slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index);
|
||||
});
|
||||
|
||||
private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
|
||||
private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
|
||||
|
||||
private void ensureSnakingOut(double startTime, int repeatIndex)
|
||||
{
|
||||
var repeatTime = timeAtRepeat(startTime, repeatIndex);
|
||||
|
||||
if (repeatIndex % 2 == 0)
|
||||
checkPositionChange(repeatTime, sliderStart, positionIncreased);
|
||||
else
|
||||
checkPositionChange(repeatTime, sliderEnd, positionDecreased);
|
||||
}
|
||||
|
||||
private void ensureNoSnakingOut(double startTime, int repeatIndex) =>
|
||||
checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
|
||||
|
||||
private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
|
||||
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)sliderStart : sliderEnd;
|
||||
|
||||
private List<Vector2> sliderCurve => ((PlaySliderBody)slider.Body.Drawable).CurrentCurve;
|
||||
private Vector2 sliderStart() => sliderCurve.First();
|
||||
private Vector2 sliderEnd() => sliderCurve.Last();
|
||||
|
||||
private Vector2 sliderRepeat()
|
||||
{
|
||||
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1);
|
||||
var repeat = drawable.ChildrenOfType<Container<DrawableSliderRepeat>>().First().Children.First();
|
||||
return repeat.Position;
|
||||
}
|
||||
|
||||
private bool positionRemainsSame(Vector2 previous, Vector2 current) => previous == current;
|
||||
private bool positionIncreased(Vector2 previous, Vector2 current) => current.X > previous.X && current.Y > previous.Y;
|
||||
private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y;
|
||||
private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1);
|
||||
|
||||
private void checkPositionChange(double startTime, Func<Vector2> positionToCheck, Func<Vector2, Vector2, bool> positionAssertion)
|
||||
{
|
||||
Vector2 previousPosition = Vector2.Zero;
|
||||
|
||||
string positionDescription = positionToCheck.Method.Name.Humanize(LetterCasing.LowerCase);
|
||||
string assertionDescription = positionAssertion.Method.Name.Humanize(LetterCasing.LowerCase);
|
||||
|
||||
addSeekStep(startTime);
|
||||
AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke());
|
||||
addSeekStep(startTime + 100);
|
||||
AddAssert($"{positionDescription} {assertionDescription}", () =>
|
||||
{
|
||||
var currentPosition = positionToCheck.Invoke();
|
||||
return positionAssertion.Invoke(previousPosition, currentPosition);
|
||||
});
|
||||
}
|
||||
|
||||
private void setSnaking(bool value)
|
||||
{
|
||||
AddStep($"{(value ? "enable" : "disable")} snaking", () =>
|
||||
{
|
||||
snakingIn.Value = value;
|
||||
snakingOut.Value = value;
|
||||
});
|
||||
}
|
||||
|
||||
private void addSeekStep(double time)
|
||||
{
|
||||
AddStep($"seek to {time}", () => track.Seek(time));
|
||||
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||
{
|
||||
HitObjects = hitObjects
|
||||
};
|
||||
|
||||
private readonly List<HitObject> hitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(300, 200)
|
||||
}),
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = 13000,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(300, 200)
|
||||
}),
|
||||
RepeatCount = 1,
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = 23000,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(300, 200)
|
||||
}),
|
||||
RepeatCount = 2,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 199999,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -6,6 +6,7 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
{
|
||||
@ -28,16 +29,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
circlePiece.UpdateFrom(HitObject);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
BeginPlacement();
|
||||
HitObject.Position = ToLocalSpace(screenSpacePosition);
|
||||
}
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition);
|
||||
}
|
||||
}
|
||||
|
@ -16,22 +16,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
public class PathControlPointConnectionPiece : CompositeDrawable
|
||||
{
|
||||
public PathControlPoint ControlPoint;
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
private readonly Path path;
|
||||
private readonly Slider slider;
|
||||
private readonly int controlPointIndex;
|
||||
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<int> pathVersion;
|
||||
|
||||
public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint)
|
||||
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
|
||||
{
|
||||
this.slider = slider;
|
||||
ControlPoint = controlPoint;
|
||||
this.controlPointIndex = controlPointIndex;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
ControlPoint = slider.Path.ControlPoints[controlPointIndex];
|
||||
|
||||
InternalChild = path = new SmoothPath
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -61,13 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
path.ClearVertices();
|
||||
|
||||
int index = slider.Path.ControlPoints.IndexOf(ControlPoint) + 1;
|
||||
|
||||
if (index == 0 || index == slider.Path.ControlPoints.Count)
|
||||
int nextIndex = controlPointIndex + 1;
|
||||
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
|
||||
return;
|
||||
|
||||
path.AddVertex(Vector2.Zero);
|
||||
path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
|
||||
path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value);
|
||||
|
||||
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -12,6 +13,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
@ -26,13 +28,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
|
||||
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
private readonly Slider slider;
|
||||
private readonly Container marker;
|
||||
private readonly Drawable markerRing;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider snapProvider { get; set; }
|
||||
|
||||
@ -47,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
this.slider = slider;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
@ -137,7 +143,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left;
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (RequestSelection == null)
|
||||
return false;
|
||||
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
@ -158,6 +176,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
ControlPoint.Position.Value += e.Delta;
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the circular control point marker.
|
||||
/// </summary>
|
||||
@ -168,8 +188,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
markerRing.Alpha = IsSelected.Value ? 1 : 0;
|
||||
|
||||
Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
|
||||
|
||||
if (IsHovered || IsSelected.Value)
|
||||
colour = Color4.White;
|
||||
colour = colour.Lighten(1);
|
||||
|
||||
marker.Colour = colour;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Bindables;
|
||||
@ -24,17 +25,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
{
|
||||
internal readonly Container<PathControlPointPiece> Pieces;
|
||||
internal readonly Container<PathControlPointConnectionPiece> Connections;
|
||||
|
||||
private readonly Container<PathControlPointConnectionPiece> connections;
|
||||
|
||||
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
||||
private readonly Slider slider;
|
||||
|
||||
private readonly bool allowSelection;
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
private IBindableList<PathControlPoint> controlPoints;
|
||||
|
||||
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
||||
|
||||
public PathControlPointVisualiser(Slider slider, bool allowSelection)
|
||||
@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
|
||||
Connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
|
||||
Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
}
|
||||
@ -57,33 +55,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
controlPoints = slider.Path.ControlPoints.GetBoundCopy();
|
||||
controlPoints.ItemsAdded += addControlPoints;
|
||||
controlPoints.ItemsRemoved += removeControlPoints;
|
||||
|
||||
addControlPoints(controlPoints);
|
||||
controlPoints.CollectionChanged += onControlPointsChanged;
|
||||
controlPoints.BindTo(slider.Path.ControlPoints);
|
||||
}
|
||||
|
||||
private void addControlPoints(IEnumerable<PathControlPoint> controlPoints)
|
||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
foreach (var point in controlPoints)
|
||||
switch (e.Action)
|
||||
{
|
||||
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
|
||||
{
|
||||
if (allowSelection)
|
||||
d.RequestSelection = selectPiece;
|
||||
}));
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
{
|
||||
var point = (PathControlPoint)e.NewItems[i];
|
||||
|
||||
connections.Add(new PathControlPointConnectionPiece(slider, point));
|
||||
}
|
||||
}
|
||||
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
|
||||
{
|
||||
if (allowSelection)
|
||||
d.RequestSelection = selectPiece;
|
||||
}));
|
||||
|
||||
private void removeControlPoints(IEnumerable<PathControlPoint> controlPoints)
|
||||
{
|
||||
foreach (var point in controlPoints)
|
||||
{
|
||||
Pieces.RemoveAll(p => p.ControlPoint == point);
|
||||
connections.RemoveAll(c => c.ControlPoint == point);
|
||||
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
||||
{
|
||||
Pieces.RemoveAll(p => p.ControlPoint == point);
|
||||
Connections.RemoveAll(c => c.ControlPoint == point);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
@ -23,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private SliderBodyPiece bodyPiece;
|
||||
private HitCirclePiece headCirclePiece;
|
||||
private HitCirclePiece tailCirclePiece;
|
||||
private PathControlPointVisualiser controlPointVisualiser;
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
@ -51,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
bodyPiece = new SliderBodyPiece(),
|
||||
headCirclePiece = new HitCirclePiece(),
|
||||
tailCirclePiece = new HitCirclePiece(),
|
||||
new PathControlPointVisualiser(HitObject, false)
|
||||
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
|
||||
};
|
||||
|
||||
setState(PlacementState.Initial);
|
||||
@ -73,17 +77,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
break;
|
||||
|
||||
case PlacementState.Body:
|
||||
ensureCursor();
|
||||
|
||||
// The given screen-space position may have been externally snapped, but the unsnapped position from the input manager
|
||||
// is used instead since snapping control points doesn't make much sense
|
||||
cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
updateCursor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
return base.OnMouseDown(e);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case PlacementState.Initial:
|
||||
@ -91,14 +94,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
break;
|
||||
|
||||
case PlacementState.Body:
|
||||
switch (e.Button)
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
{
|
||||
case MouseButton.Left:
|
||||
ensureCursor();
|
||||
// Place a new point by detatching the current cursor.
|
||||
updateCursor();
|
||||
cursor = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transform the last point into a new segment.
|
||||
Debug.Assert(lastPoint != null);
|
||||
|
||||
// Detatch the cursor
|
||||
cursor = null;
|
||||
break;
|
||||
segmentStart = lastPoint;
|
||||
segmentStart.Type.Value = PathType.Linear;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
}
|
||||
|
||||
break;
|
||||
@ -114,16 +124,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override bool OnDoubleClick(DoubleClickEvent e)
|
||||
{
|
||||
// Todo: This should all not occur on double click, but rather if the previous control point is hovered.
|
||||
segmentStart = HitObject.Path.ControlPoints[^1];
|
||||
segmentStart.Type.Value = PathType.Linear;
|
||||
|
||||
currentSegmentLength = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void beginCurve()
|
||||
{
|
||||
BeginPlacement(commitStart: true);
|
||||
@ -161,17 +161,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureCursor()
|
||||
private void updateCursor()
|
||||
{
|
||||
if (cursor == null)
|
||||
if (canPlaceNewControlPoint(out _))
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } });
|
||||
currentSegmentLength++;
|
||||
// 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 } });
|
||||
|
||||
// The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
|
||||
currentSegmentLength++;
|
||||
updatePathType();
|
||||
}
|
||||
|
||||
// Update the cursor position.
|
||||
cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
}
|
||||
else if (cursor != null)
|
||||
{
|
||||
// The cursor overlaps a previous control point, so it's removed.
|
||||
HitObject.Path.ControlPoints.Remove(cursor);
|
||||
cursor = null;
|
||||
|
||||
// The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear).
|
||||
currentSegmentLength--;
|
||||
updatePathType();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a new control point can be placed at the current mouse position.
|
||||
/// </summary>
|
||||
/// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param>
|
||||
/// <returns>Whether a new control point can be placed at the current position.</returns>
|
||||
private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
|
||||
{
|
||||
// We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
|
||||
var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
|
||||
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
|
||||
|
||||
lastPoint = last;
|
||||
return lastPiece?.IsHovered != true;
|
||||
}
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -34,6 +35,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPlacementHandler placementHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
public SliderSelectionBlueprint(DrawableSlider slider)
|
||||
: base(slider)
|
||||
{
|
||||
@ -88,7 +95,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private int? placementControlPointIndex;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e) => placementControlPointIndex != null;
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (placementControlPointIndex != null)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
@ -99,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
placementControlPointIndex = null;
|
||||
if (placementControlPointIndex != null)
|
||||
{
|
||||
placementControlPointIndex = null;
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
}
|
||||
|
||||
private BindableList<PathControlPoint> controlPoints => HitObject.Path.ControlPoints;
|
||||
@ -162,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private void updatePath()
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
UpdateHitObject();
|
||||
editorBeatmap?.UpdateHitObject(HitObject);
|
||||
}
|
||||
|
||||
public override MenuItem[] ContextMenuItems => new MenuItem[]
|
||||
|
@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
private const double fade_in_duration_multiplier = 0.4;
|
||||
private const double fade_out_duration_multiplier = 0.3;
|
||||
|
||||
protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner);
|
||||
|
||||
public override void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
{
|
||||
static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;
|
||||
|
@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using static osu.Game.Input.Handlers.ReplayInputHandler;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
@ -33,15 +34,30 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
private ReplayState<OsuAction> state;
|
||||
private double lastStateChangeTime;
|
||||
|
||||
private bool hasReplay;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// grab the input manager for future use.
|
||||
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
if (osuInputManager.ReplayInputHandler != null)
|
||||
{
|
||||
hasReplay = true;
|
||||
return;
|
||||
}
|
||||
|
||||
osuInputManager.AllowUserPresses = false;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (hasReplay)
|
||||
return;
|
||||
|
||||
bool requiresHold = false;
|
||||
bool requiresHit = false;
|
||||
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
Anchor = Anchor.Centre,
|
||||
Alpha = 0.5f,
|
||||
}
|
||||
}, confineMode: ConfineMode.NoScaling);
|
||||
});
|
||||
}
|
||||
|
||||
public double AnimationStartTime { get; set; }
|
||||
|
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
var result = HitObject.HitWindows.ResultFor(timeOffset);
|
||||
|
||||
if (result == HitResult.None)
|
||||
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
|
||||
{
|
||||
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
|
||||
return;
|
||||
|
@ -1,11 +1,14 @@
|
||||
// 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.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@ -16,6 +19,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
|
||||
public override bool HandlePositionalInput => true;
|
||||
|
||||
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit.
|
||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
||||
/// </summary>
|
||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
||||
|
||||
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
@ -54,6 +65,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
|
||||
|
||||
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
|
||||
}
|
||||
}
|
||||
|
@ -124,8 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
case SliderTailCircle tail:
|
||||
return new DrawableSliderTail(slider, tail);
|
||||
|
||||
case HitCircle head:
|
||||
return new DrawableSliderHead(slider, head) { OnShake = Shake };
|
||||
case SliderHeadCircle head:
|
||||
return new DrawableSliderHead(slider, head)
|
||||
{
|
||||
OnShake = Shake,
|
||||
CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true
|
||||
};
|
||||
|
||||
case SliderTick tick:
|
||||
return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position };
|
||||
@ -186,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.ApplySkin(skin, allowFallback);
|
||||
|
||||
bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
|
||||
Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White;
|
||||
Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private readonly Slider slider;
|
||||
|
||||
public DrawableSliderHead(Slider slider, HitCircle h)
|
||||
public DrawableSliderHead(Slider slider, SliderHeadCircle h)
|
||||
: base(h)
|
||||
{
|
||||
this.slider = slider;
|
||||
|
@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
Blending = BlendingParameters.Additive;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = scaleContainer = new ReverseArrowPiece();
|
||||
|
@ -8,11 +8,16 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class ReverseArrowPiece : BeatSyncedContainer
|
||||
{
|
||||
[Resolved]
|
||||
private DrawableHitObject drawableRepeat { get; set; }
|
||||
|
||||
public ReverseArrowPiece()
|
||||
{
|
||||
Divisor = 2;
|
||||
@ -21,13 +26,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Blending = BlendingParameters.Additive;
|
||||
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Size = new Vector2(0.35f)
|
||||
})
|
||||
@ -37,7 +41,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) =>
|
||||
Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
|
||||
{
|
||||
if (!drawableRepeat.IsHit)
|
||||
Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
this.drawableSlider = drawableSlider;
|
||||
this.slider = slider;
|
||||
|
||||
Blending = BlendingParameters.Additive;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
BorderThickness = 10,
|
||||
BorderColour = Color4.White,
|
||||
Alpha = 1,
|
||||
|
@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
break;
|
||||
|
||||
case SliderEventType.Head:
|
||||
AddNested(HeadCircle = new SliderCircle
|
||||
AddNested(HeadCircle = new SliderHeadCircle
|
||||
{
|
||||
StartTime = e.Time,
|
||||
Position = Position,
|
||||
|
9
osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
Normal file
9
osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs
Normal file
@ -0,0 +1,9 @@
|
||||
// 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.Osu.Objects
|
||||
{
|
||||
public class SliderHeadCircle : HitCircle
|
||||
{
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
switch (osuComponent.Component)
|
||||
{
|
||||
case OsuSkinComponents.FollowPoint:
|
||||
return this.GetAnimation(component.LookupName, true, false, true);
|
||||
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
|
||||
|
||||
case OsuSkinComponents.SliderFollowCircle:
|
||||
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
|
||||
@ -132,6 +132,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
return SkinUtils.As<TValue>(new BindableFloat(LEGACY_CIRCLE_RADIUS));
|
||||
|
||||
break;
|
||||
|
||||
case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
|
||||
// See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
|
||||
// HitCircleOverlayAboveNumer (with typo) should still be supported for now.
|
||||
return source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
|
||||
source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
AllowSliderBallTint,
|
||||
CursorExpand,
|
||||
CursorRotate,
|
||||
HitCircleOverlayAboveNumber
|
||||
HitCircleOverlayAboveNumber,
|
||||
HitCircleOverlayAboveNumer // Some old skins will have this typo
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user