Merge remote-tracking branch 'upstream/master' into add-interval-list

This commit is contained in:
Salman Ahmed 2020-04-29 05:13:17 +03:00
commit 6fa9feb15c
No known key found for this signature in database
GPG Key ID: ED81FD33FD9B58BC
343 changed files with 8443 additions and 2450 deletions

View File

@ -4,3 +4,5 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.

View File

@ -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.407.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.421.0" />
</ItemGroup>
</Project>

View File

@ -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>

View 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)
{
}
}
}
}

View File

@ -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" />

View File

@ -71,8 +71,7 @@ protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(I
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[]
{

View File

@ -9,17 +9,26 @@
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

View File

@ -70,6 +70,8 @@ public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X;
protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject)
{

View File

@ -7,6 +7,7 @@
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 @@ private void load(DrawableHitObject drawableObject)
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 @@ private void load(DrawableHitObject drawableObject)
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Color4.Red,
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
});

View File

@ -65,6 +65,15 @@ public Drawable GetDrawableComponent(ISkinComponent component)
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);
}
}
}

View 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,
}
}

View File

@ -7,6 +7,7 @@
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 @@ private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
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);

View File

@ -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 @@ public class Catcher : Container, IKeyBindingHandler<CatchAction>
/// </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 @@ protected bool Trail
}
}
/// <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 @@ public Catcher(BeatmapDifficulty difficulty = null)
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 @@ private void load()
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 @@ public void PlaceOnPlate(DrawableCatchHitObject fruit)
/// <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;

View 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));
}
}
}

View File

@ -41,8 +41,6 @@ protected ManiaPlacementBlueprintTestScene()
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

View File

@ -0,0 +1,6 @@
[General]
Version: 2.4
[Mania]
Keys: 4
ColumnLineWidth: 3,1,3,1,1

View File

@ -37,7 +37,7 @@ private void load()
{
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
}

View File

@ -27,7 +27,6 @@ public class TestSceneHoldNoteInput : RateAdjustedBeatmapTestScene
private const double time_after_tail = 5250;
private List<JudgementResult> judgementResults;
private bool allJudgedFired;
/// <summary>
/// -----[ ]-----
@ -283,20 +282,15 @@ private void performTest(List<ReplayFrame> frames)
{
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

View File

@ -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();
}

View File

@ -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" />

View File

@ -13,7 +13,6 @@
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 @@ public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
{
TargetColumns = (int)Math.Max(1, roundedCircleSize);
if (TargetColumns >= 10)
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
TargetColumns /= 2;
Dual = true;
@ -67,7 +66,7 @@ public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
}
}
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 @@ private Pattern generate()
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 @@ private Pattern generate()
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];
}
}
}
}

View File

@ -505,16 +505,14 @@ private void addToPattern(Pattern pattern, int column, double startTime, double
}
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);

View File

@ -64,21 +64,14 @@ private void addToPattern(Pattern pattern, int column, bool holdNote)
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
{

View 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);
}
}
}

View File

@ -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 @@ protected override void Update()
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)

View File

@ -45,6 +45,7 @@ protected override void LoadComplete()
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 1,
BorderColour = colours.Yellow,
Child = new Box

View File

@ -50,16 +50,10 @@ protected override bool OnMouseDown(MouseDownEvent e)
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)

View File

@ -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 @@ protected override void Update()
Width = SnappedWidth;
Position = SnappedMousePosition;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
base.OnMouseDown(e);
// Place the note immediately.
EndPlacement(true);
return true;
}
}
}

View File

@ -78,5 +78,11 @@ public enum ManiaAction
[Description("Key 18")]
Key18,
[Description("Key 19")]
Key19,
[Description("Key 20")]
Key20,
}
}

View File

@ -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 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
new ManiaModKey7(),
new ManiaModKey8(),
new ManiaModKey9(),
new ManiaModKey10(),
new ManiaModKey1(),
new ManiaModKey2(),
new ManiaModKey3()),
@ -250,9 +256,9 @@ public override IEnumerable<int> AvailableVariants
{
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 @@ public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0)
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 @@ private PlayfieldType getPlayfieldType(int variant)
{
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

View 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.";
}
}

View File

@ -51,7 +51,10 @@ public DrawableHoldNote(HoldNote hitObject)
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 @@ protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection>
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();

View File

@ -7,6 +7,7 @@
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 @@ public abstract class DrawableManiaHitObject : DrawableHitObject<ManiaHitObject>
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)
{

View File

@ -34,7 +34,6 @@ public class DefaultBodyPiece : CompositeDrawable
public DefaultBodyPiece()
{
RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
AddLayout(subtractionCache);

View File

@ -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 @@ public double Duration
set
{
duration = value;
Tail.StartTime = EndTime;
if (Tail != null)
Tail.StartTime = EndTime;
}
}
@ -38,8 +42,12 @@ public override double StartTime
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 @@ public override int Column
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 @@ protected override void CreateNestedHitObjects()
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 @@ private void createTicks()
public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}

View File

@ -5,11 +5,12 @@
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 @@ public virtual int Column
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
#region LegacyBeatmapEncoder
float IHasXPosition.X => Column;
#endregion
}
}

View File

@ -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 @@ public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFr
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 @@ public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
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));
}
}
}

View 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 _);
}
}

View File

@ -67,6 +67,7 @@ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
RelativeSizeAxes = Axes.Y,
Width = leftLineWidth,
Scale = new Vector2(0.740f, 1),
Colour = lineColour,
Alpha = hasLeftLine ? 1 : 0
},
@ -76,6 +77,7 @@ private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = rightLineWidth,
Scale = new Vector2(0.740f, 1),
Colour = lineColour,
Alpha = hasRightLine ? 1 : 0
},

View File

@ -6,6 +6,7 @@
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,6 +15,7 @@
namespace osu.Game.Rulesets.Mania.UI
{
[Cached]
public class ManiaPlayfield : ScrollingPlayfield
{
private readonly List<Stage> stages = new List<Stage>();

View 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;
}
}
}

View 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;
}
}
}

View 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)
{
}
}
}
}

View 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;
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)));
}
}

View File

@ -47,7 +47,6 @@ public class TestSceneSliderInput : RateAdjustedBeatmapTestScene
private const double time_slider_end = 4000;
private List<JudgementResult> judgementResults;
private bool allJudgedFired;
/// <summary>
/// Scenario:
@ -375,20 +374,15 @@ private void performTest(List<ReplayFrame> frames)
{
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

View File

@ -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();
}

View File

@ -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" />

View File

@ -6,6 +6,7 @@
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 @@ protected override void Update()
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);
}
}

View File

@ -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 @@ private void updateConnectingPath()
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);
}

View File

@ -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.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 @@ public class PathControlPointPiece : BlueprintPiece<Slider>
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 @@ public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
this.slider = slider;
ControlPoint = controlPoint;
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@ -137,7 +143,19 @@ protected override bool OnMouseDown(MouseDownEvent e)
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 @@ protected override void OnDrag(DragEvent e)
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 @@ private void updateMarkerDisplay()
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;
}
}

View File

@ -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 @@ public PathControlPointVisualiser(Slider slider, bool allowSelection)
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 @@ protected override void LoadComplete()
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;
}
}

View File

@ -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 @@ public class SliderPlacementBlueprint : PlacementBlueprint
private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece;
private HitCirclePiece tailCirclePiece;
private PathControlPointVisualiser controlPointVisualiser;
private InputManager inputManager;
@ -51,7 +55,7 @@ private void load(OsuColour colours)
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 @@ public override void UpdatePosition(Vector2 screenSpacePosition)
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 @@ protected override bool OnClick(ClickEvent e)
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 @@ protected override void OnMouseUp(MouseUpEvent e)
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 @@ private void updatePathType()
}
}
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;

View File

@ -38,6 +38,9 @@ public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider>
[Resolved(CanBeNull = true)]
private EditorBeatmap editorBeatmap { get; set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@ -92,7 +95,16 @@ protected override bool OnMouseDown(MouseDownEvent e)
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)
{
@ -103,7 +115,11 @@ protected override void OnDrag(DragEvent e)
protected override void OnDragEnd(DragEndEvent e)
{
placementControlPointIndex = null;
if (placementControlPointIndex != null)
{
placementControlPointIndex = null;
changeHandler?.EndChange();
}
}
private BindableList<PathControlPoint> controlPoints => HitObject.Path.ControlPoints;

View File

@ -23,6 +23,8 @@ public class OsuModHidden : ModHidden
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;

View File

@ -11,11 +11,12 @@
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 @@ public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawabl
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;

View File

@ -120,7 +120,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
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;

View File

@ -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 @@ public class DrawableOsuHitObject : DrawableHitObject<OsuHitObject>
// 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 @@ protected override void UpdateStateTransforms(ArmedState state)
}
}
/// <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);
}
}

View File

@ -124,8 +124,12 @@ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
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 };

View File

@ -18,7 +18,7 @@ public class DrawableSliderHead : DrawableHitCircle
private readonly Slider slider;
public DrawableSliderHead(Slider slider, HitCircle h)
public DrawableSliderHead(Slider slider, SliderHeadCircle h)
: base(h)
{
this.slider = slider;

View File

@ -155,7 +155,7 @@ protected override void CreateNestedHitObjects()
break;
case SliderEventType.Head:
AddNested(HeadCircle = new SliderCircle
AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,

View 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
{
}
}

View 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;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Ensures that <see cref="HitObject"/>s are hit in-order. Affectionately known as "note lock".
/// If a <see cref="HitObject"/> is hit out of order:
/// <list type="number">
/// <item><description>The hit is blocked if it occurred earlier than the previous <see cref="HitObject"/>'s start time.</description></item>
/// <item><description>The hit causes all previous <see cref="HitObject"/>s to missed otherwise.</description></item>
/// </list>
/// </summary>
public class OrderedHitPolicy
{
private readonly HitObjectContainer hitObjectContainer;
public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
{
this.hitObjectContainer = hitObjectContainer;
}
/// <summary>
/// Determines whether a <see cref="DrawableHitObject"/> can be hit at a point in time.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
/// <param name="time">The time to check.</param>
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
public bool IsHittable(DrawableHitObject hitObject, double time)
{
DrawableHitObject blockingObject = null;
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
if (hitObjectCanBlockFutureHits(obj))
blockingObject = obj;
}
// If there is no previous hitobject, allow the hit.
if (blockingObject == null)
return true;
// A hit is allowed if:
// 1. The last blocking hitobject has been judged.
// 2. The current time is after the last hitobject's start time.
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
}
/// <summary>
/// Handles a <see cref="HitObject"/> being hit to potentially miss all earlier <see cref="HitObject"/>s.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> that was hit.</param>
public void HandleHit(DrawableHitObject hitObject)
{
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
if (!hitObjectCanBlockFutureHits(hitObject))
return;
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
if (obj.Judged)
continue;
if (hitObjectCanBlockFutureHits(obj))
((DrawableOsuHitObject)obj).MissForcefully();
}
}
/// <summary>
/// Whether a <see cref="HitObject"/> blocks hits on future <see cref="HitObject"/>s until its start time is reached.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to test.</param>
private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject)
=> hitObject is DrawableHitCircle;
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
{
foreach (var obj in hitObjectContainer.AliveObjects)
{
if (obj.HitObject.StartTime >= targetTime)
yield break;
yield return obj;
foreach (var nestedObj in obj.NestedHitObjects)
{
if (nestedObj.HitObject.StartTime >= targetTime)
break;
yield return nestedObj;
}
}
}
}
}

View File

@ -20,6 +20,7 @@ public class OsuPlayfield : Playfield
private readonly ApproachCircleProxyContainer approachCircles;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly OrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -51,6 +52,8 @@ public OsuPlayfield()
Depth = -1,
},
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
}
public override void Add(DrawableHitObject h)
@ -64,7 +67,10 @@ public override void Add(DrawableHitObject h)
base.Add(h);
followPoints.AddFollowPoints((DrawableOsuHitObject)h);
DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h;
osuHitObject.CheckHittable = hitPolicy.IsHittable;
followPoints.AddFollowPoints(osuHitObject);
}
public override bool Remove(DrawableHitObject h)
@ -79,6 +85,9 @@ public override bool Remove(DrawableHitObject h)
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
hitPolicy.HandleHit(judgedObject);
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;

View File

@ -0,0 +1,29 @@
// 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.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
internal class DrawableTestHit : DrawableTaikoHitObject
{
private readonly HitResult type;
public DrawableTestHit(Hit hit, HitResult type = HitResult.Great)
: base(hit)
{
this.type = type;
}
[BackgroundDependencyLoader]
private void load()
{
Result.Type = type;
}
public override bool OnPressed(TaikoAction action) => false;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -6,7 +6,7 @@
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public abstract class TaikoSkinnableTestScene : SkinnableTestScene
{

View File

@ -0,0 +1,111 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableBarLine : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableBarLine),
typeof(LegacyBarLine),
typeof(BarLine),
}).ToList();
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 5000 },
};
[BackgroundDependencyLoader]
private void load()
{
AddStep("Bar line", () => SetContents(() =>
{
ScrollingHitObjectContainer hoc;
var cont = new Container
{
RelativeSizeAxes = Axes.Both,
Height = 0.8f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new TaikoPlayfield(new ControlPointInfo()),
hoc = new ScrollingHitObjectContainer()
}
};
hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
return cont;
}));
AddStep("Bar line (major)", () => SetContents(() =>
{
ScrollingHitObjectContainer hoc;
var cont = new Container
{
RelativeSizeAxes = Axes.Both,
Height = 0.8f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new TaikoPlayfield(new ControlPointInfo()),
hoc = new ScrollingHitObjectContainer()
}
};
hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
return cont;
}));
}
private BarLine createBarLineAtCurrentTime(bool major = false)
{
var barline = new BarLine
{
Major = major,
StartTime = Time.Current + 2000,
};
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
barline.ApplyDefaults(cpi, new BeatmapDifficulty());
return barline;
}
}
}

View File

@ -0,0 +1,86 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableDrumRoll : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableDrumRoll),
typeof(DrawableDrumRollTick),
typeof(LegacyDrumRoll),
}).ToList();
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 5000 },
};
[BackgroundDependencyLoader]
private void load()
{
AddStep("Drum roll", () => SetContents(() =>
{
var hoc = new ScrollingHitObjectContainer();
hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
});
return hoc;
}));
AddStep("Drum roll (strong)", () => SetContents(() =>
{
var hoc = new ScrollingHitObjectContainer();
hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
});
return hoc;
}));
}
private DrumRoll createDrumRollAtCurrentTime(bool strong = false)
{
var drumroll = new DrumRoll
{
IsStrong = strong,
StartTime = Time.Current + 1000,
Duration = 4000,
};
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
drumroll.ApplyDefaults(cpi, new BeatmapDifficulty());
return drumroll;
}
}
}

View File

@ -0,0 +1,69 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableHit),
typeof(LegacyHit),
typeof(LegacyCirclePiece),
}).ToList();
[BackgroundDependencyLoader]
private void load()
{
AddStep("Centre hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
AddStep("Centre hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
AddStep("Rim hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
AddStep("Rim hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
}
private Hit createHitAtCurrentTime(bool strong = false)
{
var hit = new Hit
{
IsStrong = strong,
StartTime = Time.Current + 3000,
};
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return hit;
}
}
}

View File

@ -0,0 +1,58 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(HitExplosion),
typeof(LegacyHitExplosion),
typeof(DefaultHitExplosion),
}).ToList();
[BackgroundDependencyLoader]
private void load()
{
AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
}
private Drawable getContentFor(HitResult type)
{
DrawableTaikoHitObject hit;
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hit = createHit(type),
new HitExplosion(hit)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
}
private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
}
}

View File

@ -6,14 +6,14 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneInputDrum : TaikoSkinnableTestScene

View File

@ -0,0 +1,46 @@
// 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.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
public class TestSceneTaikoPlayfield : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(TaikoHitTarget),
typeof(TaikoLegacyHitTarget),
typeof(PlayfieldBackgroundRight),
}).ToList();
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 5000 },
};
public TestSceneTaikoPlayfield()
{
AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo())
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}));
AddRepeatStep("change height", () => this.ChildrenOfType<TaikoPlayfield>().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
}
}
}

View File

@ -24,9 +24,9 @@
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestSceneTaikoPlayfield : OsuTestScene
public class TestSceneHits : OsuTestScene
{
private const double default_duration = 1000;
private const double default_duration = 3000;
private const float scroll_time = 1000;
protected override double TimePerAction => default_duration * 2;
@ -45,6 +45,7 @@ private void load()
AddStep("Miss :(", addMissJudgement);
AddStep("DrumRoll", () => addDrumRoll(false));
AddStep("Strong DrumRoll", () => addDrumRoll(true));
AddStep("Kiai DrumRoll", () => addDrumRoll(true, kiai: true));
AddStep("Swell", () => addSwell());
AddStep("Centre", () => addCentreHit(false));
AddStep("Strong Centre", () => addCentreHit(true));
@ -148,6 +149,8 @@ private void addHitJudgement(bool kiai)
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
Add(h);
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
}
@ -163,6 +166,8 @@ private void addStrongHitJudgement(bool kiai)
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
Add(h);
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
}
@ -192,7 +197,7 @@ private void addSwell(double duration = default_duration)
drawableRuleset.Playfield.Add(new DrawableSwell(swell));
}
private void addDrumRoll(bool strong, double duration = default_duration)
private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false)
{
addBarLine(true);
addBarLine(true, scroll_time + duration);
@ -202,9 +207,13 @@ private void addDrumRoll(bool strong, double duration = default_duration)
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong,
Duration = duration,
TickRate = 8,
};
d.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var cpi = new ControlPointInfo();
cpi.Add(-10000, new EffectControlPoint { KiaiMode = kiai });
d.ApplyDefaults(cpi, new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
}
@ -219,7 +228,7 @@ private void addCentreHit(bool strong)
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableCentreHit(h));
drawableRuleset.Playfield.Add(new DrawableHit(h));
}
private void addRimHit(bool strong)
@ -232,7 +241,7 @@ private void addRimHit(bool strong)
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableRimHit(h));
drawableRuleset.Playfield.Add(new DrawableHit(h));
}
private class TestStrongNestedHit : DrawableStrongNestedHit
@ -244,13 +253,5 @@ public TestStrongNestedHit(DrawableHitObject mainObject)
public override bool OnPressed(TaikoAction action) => false;
}
private class DrawableTestHit : DrawableHitObject<TaikoHitObject>
{
public DrawableTestHit(TaikoHitObject hitObject)
: base(hitObject)
{
}
}
}
}

View File

@ -19,7 +19,7 @@ public TestSceneSwellJudgements()
[Test]
public void TestZeroTickTimeOffsets()
{
AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted);
AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value);
AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
}

View File

@ -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" />

View File

@ -19,7 +19,7 @@ internal class TaikoBeatmapConverter : BeatmapConverter<TaikoHitObject>
/// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used.
/// </summary>
private const float legacy_velocity_multiplier = 1.4f;
public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
/// <summary>
/// Because swells are easier in taiko than spinners are in osu!,
@ -52,7 +52,7 @@ protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original)
// Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original);
@ -92,7 +92,7 @@ protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, I
double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
double distance = distanceData.Distance * spans * legacy_velocity_multiplier;
double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;

View File

@ -6,6 +6,7 @@
using osu.Game.Rulesets.Objects;
using osuTK;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@ -27,7 +28,7 @@ public class DrawableBarLine : DrawableHitObject<HitObject>
/// <summary>
/// The visual line tracker.
/// </summary>
protected Box Tracker;
protected SkinnableDrawable Line;
/// <summary>
/// The bar line.
@ -45,13 +46,15 @@ public DrawableBarLine(BarLine barLine)
RelativeSizeAxes = Axes.Y;
Width = tracker_width;
AddInternal(Tracker = new Box
AddInternal(Line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box
{
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(0.5f, 0),
})
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(0.5f, 0),
Alpha = 0.75f
Alpha = 0.75f,
});
}

View File

@ -53,7 +53,7 @@ public DrawableBarLineMajor(BarLine barLine)
}
});
Tracker.Alpha = 1f;
Line.Alpha = 1f;
}
protected override void LoadComplete()

View File

@ -1,26 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableCentreHit : DrawableHit
{
public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre };
public DrawableCentreHit(Hit hit)
: base(hit)
{
MainPiece.Add(new CentreHitSymbolPiece());
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
MainPiece.AccentColour = colours.PinkDarker;
}
}
}

View File

@ -14,6 +14,8 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@ -29,7 +31,7 @@ public class DrawableDrumRoll : DrawableTaikoHitObject<DrumRoll>
/// </summary>
private int rollingHits;
private readonly Container<DrawableDrumRollTick> tickContainer;
private Container tickContainer;
private Color4 colourIdle;
private Color4 colourEngaged;
@ -38,14 +40,20 @@ public DrawableDrumRoll(DrumRoll drumRoll)
: base(drumRoll)
{
RelativeSizeAxes = Axes.Y;
MainPiece.Add(tickContainer = new Container<DrawableDrumRollTick> { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
MainPiece.AccentColour = colourIdle = colours.YellowDark;
colourIdle = colours.YellowDark;
colourEngaged = colours.YellowDarker;
updateColour();
Content.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
if (MainPiece.Drawable is IHasAccentColour accentMain)
accentMain.AccentColour = colourIdle;
}
protected override void LoadComplete()
@ -84,7 +92,8 @@ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
return base.CreateNestedHitObject(hitObject);
}
protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece();
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollBody),
_ => new ElongatedCirclePiece());
public override bool OnPressed(TaikoAction action) => false;
@ -100,8 +109,7 @@ private void onNewResult(DrawableHitObject obj, JudgementResult result)
rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
MainPiece.FadeAccent(newColour, 100);
updateColour();
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
@ -113,8 +121,11 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
return;
int countHit = NestedHitObjects.Count(o => o.IsHit);
if (countHit >= HitObject.RequiredGoodHits)
{
ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good);
}
else
ApplyResult(r => r.Type = HitResult.Miss);
}
@ -130,8 +141,22 @@ protected override void UpdateStateTransforms(ArmedState state)
}
}
protected override void Update()
{
base.Update();
OriginPosition = new Vector2(DrawHeight);
Content.X = DrawHeight / 2;
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
private void updateColour()
{
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100);
}
private class StrongNestedHit : DrawableStrongNestedHit
{
public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll)

View File

@ -6,23 +6,28 @@
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableDrumRollTick : DrawableTaikoHitObject<DrumRollTick>
{
/// <summary>
/// The hit type corresponding to the <see cref="TaikoAction"/> that the user pressed to hit this <see cref="DrawableDrumRollTick"/>.
/// </summary>
public HitType JudgementType;
public DrawableDrumRollTick(DrumRollTick tick)
: base(tick)
{
FillMode = FillMode.Fit;
}
public override bool DisplayResult => false;
protected override TaikoPiece CreateMainPiece() => new TickPiece
{
Filled = HitObject.FirstTick
};
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
_ => new TickPiece
{
Filled = HitObject.FirstTick
});
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
@ -49,7 +54,11 @@ protected override void UpdateStateTransforms(ArmedState state)
}
}
public override bool OnPressed(TaikoAction action) => UpdateResult(true);
public override bool OnPressed(TaikoAction action)
{
JudgementType = action == TaikoAction.LeftRim || action == TaikoAction.RightRim ? HitType.Rim : HitType.Centre;
return UpdateResult(true);
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
/// <summary>
/// A hit used specifically for drum rolls, where spawning flying hits is required.
/// </summary>
public class DrawableFlyingHit : DrawableHit
{
public DrawableFlyingHit(DrawableDrumRollTick drumRollTick)
: base(new IgnoreHit
{
StartTime = drumRollTick.HitObject.StartTime + drumRollTick.Result.TimeOffset,
IsStrong = drumRollTick.HitObject.IsStrong,
Type = drumRollTick.JudgementType
})
{
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
}
protected override void LoadComplete()
{
base.LoadComplete();
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
}
}

View File

@ -8,31 +8,45 @@
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public abstract class DrawableHit : DrawableTaikoHitObject<Hit>
public class DrawableHit : DrawableTaikoHitObject<Hit>
{
/// <summary>
/// A list of keys which can result in hits for this HitObject.
/// </summary>
public abstract TaikoAction[] HitActions { get; }
public TaikoAction[] HitActions { get; }
/// <summary>
/// The action that caused this <see cref="DrawableHit"/> to be hit.
/// </summary>
public TaikoAction? HitAction { get; private set; }
public TaikoAction? HitAction
{
get;
private set;
}
private bool validActionPressed;
private bool pressHandledThisFrame;
protected DrawableHit(Hit hit)
public DrawableHit(Hit hit)
: base(hit)
{
FillMode = FillMode.Fit;
HitActions =
HitObject.Type == HitType.Centre
? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
: new[] { TaikoAction.LeftRim, TaikoAction.RightRim };
}
protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre
? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
: new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
@ -58,7 +72,6 @@ public override bool OnPressed(TaikoAction action)
{
if (pressHandledThisFrame)
return true;
if (Judged)
return false;
@ -66,14 +79,12 @@ public override bool OnPressed(TaikoAction action)
// Only count this as handled if the new judgement is a hit
var result = UpdateResult(true);
if (IsHit)
HitAction = action;
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
pressHandledThisFrame = true;
return result;
}
@ -81,7 +92,6 @@ public override void OnReleased(TaikoAction action)
{
if (action == HitAction)
HitAction = null;
base.OnReleased(action);
}
@ -92,8 +102,6 @@ protected override void Update()
// The input manager processes all input prior to us updating, so this is the perfect time
// for us to remove the extra press blocking, before input is handled in the next frame
pressHandledThisFrame = false;
Size = BaseSize * Parent.RelativeChildSize;
}
protected override void UpdateStateTransforms(ArmedState state)
@ -116,7 +124,7 @@ protected override void UpdateStateTransforms(ArmedState state)
// If we're far enough away from the left stage, we should bring outselves in front of it
ProxyContent();
var flash = (MainPiece as CirclePiece)?.FlashBox;
var flash = (MainPiece.Drawable as CirclePiece)?.FlashBox;
flash?.FadeTo(0.9f).FadeOut(300);
const float gravity_time = 300;

View File

@ -1,26 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableRimHit : DrawableHit
{
public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim };
public DrawableRimHit(Hit hit)
: base(hit)
{
MainPiece.Add(new RimHitSymbolPiece());
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
MainPiece.AccentColour = colours.BlueDarker;
}
}
}

View File

@ -9,11 +9,12 @@
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@ -34,8 +35,6 @@ public class DrawableSwell : DrawableTaikoHitObject<Swell>
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
private readonly SwellSymbolPiece symbol;
public DrawableSwell(Swell swell)
: base(swell)
{
@ -107,18 +106,23 @@ public DrawableSwell(Swell swell)
});
AddInternal(ticks = new Container<DrawableSwellTick> { RelativeSizeAxes = Axes.Both });
MainPiece.Add(symbol = new SwellSymbolPiece());
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
MainPiece.AccentColour = colours.YellowDark;
expandingRing.Colour = colours.YellowLight;
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
}
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Swell),
_ => new SwellCirclePiece
{
// to allow for rotation transform
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
protected override void LoadComplete()
{
base.LoadComplete();
@ -182,7 +186,7 @@ protected override void CheckForResult(bool userTriggered, double timeOffset)
.Then()
.FadeTo(completion / 8, 2000, Easing.OutQuint);
symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);

Some files were not shown because too many files have changed in this diff Show More