Merge branch 'master' of https://github.com/ppy/osu into present-recommended
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.412.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.411.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.421.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
132
osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
Normal file
@ -0,0 +1,132 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class TestSceneHyperDashColouring : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestDefaultFruitColour()
|
||||
{
|
||||
var skin = new TestSkin();
|
||||
|
||||
checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomFruitColour()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashFruitColour = Color4.Cyan
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomFruitColourPriority()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashColour = Color4.Goldenrod,
|
||||
HyperDashFruitColour = Color4.Cyan
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFruitColourFallback()
|
||||
{
|
||||
var skin = new TestSkin
|
||||
{
|
||||
HyperDashColour = Color4.Goldenrod
|
||||
};
|
||||
|
||||
checkHyperDashFruitColour(skin, skin.HyperDashColour);
|
||||
}
|
||||
|
||||
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
|
||||
{
|
||||
DrawableFruit drawableFruit = null;
|
||||
|
||||
AddStep("create hyper-dash fruit", () =>
|
||||
{
|
||||
var fruit = new Fruit { HyperDashTarget = new Banana() };
|
||||
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(4f),
|
||||
}, skin);
|
||||
});
|
||||
|
||||
AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
|
||||
}
|
||||
|
||||
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
|
||||
{
|
||||
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
|
||||
var testSkinProvider = new SkinProvidingContainer(skin);
|
||||
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
|
||||
|
||||
return legacySkinProvider
|
||||
.WithChild(testSkinProvider
|
||||
.WithChild(legacySkinTransformer
|
||||
.WithChild(child)));
|
||||
}
|
||||
|
||||
private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
|
||||
fruit.ChildrenOfType<SkinnableDrawable>().First().Drawable.ChildrenOfType<Sprite>().Any(c => c.Colour == expectedColour);
|
||||
|
||||
private class TestSkin : LegacySkin
|
||||
{
|
||||
public Color4 HyperDashColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
|
||||
}
|
||||
|
||||
public Color4 HyperDashAfterImageColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
|
||||
}
|
||||
|
||||
public Color4 HyperDashFruitColour
|
||||
{
|
||||
get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
|
||||
set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
|
||||
}
|
||||
|
||||
public TestSkin()
|
||||
: base(new SkinInfo(), null, null, string.Empty)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -71,8 +71,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
||||
{
|
||||
using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
|
||||
halfCatcherWidth = catcher.CatchWidth * 0.5f;
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
@ -9,17 +9,26 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>
|
||||
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"Use the mouse to control the catcher.";
|
||||
|
||||
private DrawableRuleset<CatchHitObject> drawableRuleset;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
if (!drawableRuleset.HasReplayLoaded.Value)
|
||||
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
|
||||
}
|
||||
|
||||
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
BorderColour = Color4.Red,
|
||||
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
BorderThickness = 12f * RADIUS_ADJUST,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
Alpha = 0.3f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Red,
|
||||
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -65,6 +65,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
|
||||
public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source.GetConfig<TLookup, TValue>(lookup);
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case CatchSkinColour colour:
|
||||
return source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
}
|
||||
|
||||
return source.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
public enum CatchSkinColour
|
||||
{
|
||||
/// <summary>
|
||||
/// The colour to be used for the catcher while in hyper-dashing state.
|
||||
/// </summary>
|
||||
HyperDash,
|
||||
|
||||
/// <summary>
|
||||
/// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
|
||||
/// </summary>
|
||||
HyperDashFruit,
|
||||
|
||||
/// <summary>
|
||||
/// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing.
|
||||
/// </summary>
|
||||
HyperDashAfterImage,
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -55,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
{
|
||||
var hyperDash = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture(lookupName),
|
||||
Colour = Color4.Red,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Depth = 1,
|
||||
Alpha = 0.7f,
|
||||
Scale = new Vector2(1.2f)
|
||||
Scale = new Vector2(1.2f),
|
||||
Texture = skin.GetTexture(lookupName),
|
||||
Colour = skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
|
||||
skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??
|
||||
Catcher.DEFAULT_HYPER_DASH_COLOUR,
|
||||
};
|
||||
|
||||
AddInternal(hyperDash);
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class Catcher : Container, IKeyBindingHandler<CatchAction>
|
||||
{
|
||||
public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we are hyper-dashing or not.
|
||||
/// </summary>
|
||||
@ -42,11 +44,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private const float allowed_catch_range = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// Width of the area that can be used to attempt catches during gameplay.
|
||||
/// </summary>
|
||||
internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range;
|
||||
|
||||
protected bool Dashing
|
||||
{
|
||||
get => dashing;
|
||||
@ -77,6 +74,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Width of the area that can be used to attempt catches during gameplay.
|
||||
/// </summary>
|
||||
private readonly float catchWidth;
|
||||
|
||||
private Container<DrawableHitObject> caughtFruit;
|
||||
|
||||
private CatcherSprite catcherIdle;
|
||||
@ -104,7 +106,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
Size = new Vector2(CatcherArea.CATCHER_SIZE);
|
||||
if (difficulty != null)
|
||||
Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
Scale = calculateScale(difficulty);
|
||||
|
||||
catchWidth = CalculateCatchWidth(Scale);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -137,6 +141,26 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
updateCatcher();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
||||
/// </summary>
|
||||
private static Vector2 calculateScale(BeatmapDifficulty difficulty)
|
||||
=> new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||
/// </summary>
|
||||
/// <param name="scale">The scale of the catcher.</param>
|
||||
internal static float CalculateCatchWidth(Vector2 scale)
|
||||
=> CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||
internal static float CalculateCatchWidth(BeatmapDifficulty difficulty)
|
||||
=> CalculateCatchWidth(calculateScale(difficulty));
|
||||
|
||||
/// <summary>
|
||||
/// Add a caught fruit to the catcher's stack.
|
||||
/// </summary>
|
||||
@ -175,7 +199,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <returns>Whether the catch is possible.</returns>
|
||||
public bool AttemptCatch(CatchHitObject fruit)
|
||||
{
|
||||
var halfCatchWidth = CatchWidth * 0.5f;
|
||||
var halfCatchWidth = catchWidth * 0.5f;
|
||||
|
||||
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
|
||||
var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
|
||||
|
51
osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaLegacyReplayTest
|
||||
{
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
|
||||
|
||||
var frame = new ManiaReplayFrame(0, actions);
|
||||
var legacyFrame = frame.ToLegacy(beatmap);
|
||||
|
||||
var decodedFrame = new ManiaReplayFrame();
|
||||
decodedFrame.FromLegacy(legacyFrame, beatmap);
|
||||
|
||||
Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
|
||||
}
|
||||
|
||||
[TestCase(ManiaAction.Key1)]
|
||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||
[TestCase(ManiaAction.Special1)]
|
||||
[TestCase(ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
|
||||
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
|
||||
[TestCase(ManiaAction.Key8)]
|
||||
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
|
||||
beatmap.Stages.Add(new StageDefinition { Columns = 5 });
|
||||
|
||||
var frame = new ManiaReplayFrame(0, actions);
|
||||
var legacyFrame = frame.ToLegacy(beatmap);
|
||||
|
||||
var decodedFrame = new ManiaReplayFrame();
|
||||
decodedFrame.FromLegacy(legacyFrame, beatmap);
|
||||
|
||||
Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions));
|
||||
}
|
||||
}
|
||||
}
|
@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AccentColour = Color4.OrangeRed,
|
||||
Clock = new FramedClock(new StopwatchClock()), // No scroll
|
||||
});
|
||||
|
||||
AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
|
||||
}
|
||||
|
||||
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
|
||||
|
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,6 @@
|
||||
[General]
|
||||
Version: 2.4
|
||||
|
||||
[Mania]
|
||||
Keys: 4
|
||||
ColumnLineWidth: 3,1,3,1,1
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
|
||||
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private const double time_after_tail = 5250;
|
||||
|
||||
private List<JudgementResult> judgementResults;
|
||||
private bool allJudgedFired;
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
@ -283,20 +282,15 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
p.ScoreProcessor.AllJudged += () =>
|
||||
{
|
||||
if (currentPlayer == p) allJudgedFired = true;
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
allJudgedFired = false;
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for all judged", () => allJudgedFired);
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
|
@ -1,17 +1,59 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
this.ChildrenOfType<HitObjectContainer>().ForEach(c => c.Clear());
|
||||
|
||||
ResetPlacement();
|
||||
|
||||
((ScrollingTestContainer)HitObjectContainer).Direction = ScrollingDirection.Down;
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestPlaceBeforeCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time < 0", () => getNote().StartTime < 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceAfterCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single()));
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("note start time > 0", () => getNote().StartTime > 0);
|
||||
}
|
||||
|
||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
||||
using osu.Game.Rulesets.Mania.MathUtils;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
||||
using osuTK;
|
||||
using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
TargetColumns = (int)Math.Max(1, roundedCircleSize);
|
||||
|
||||
if (TargetColumns >= 10)
|
||||
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
TargetColumns /= 2;
|
||||
Dual = true;
|
||||
@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject);
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
|
||||
{
|
||||
@ -239,8 +238,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTimeData.Duration,
|
||||
Column = column,
|
||||
Head = { Samples = sampleInfoListAt(HitObject.StartTime) },
|
||||
Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
});
|
||||
}
|
||||
else if (HitObject is IHasXPosition)
|
||||
@ -255,22 +254,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the sample info list at a point in time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to retrieve the sample info list from.</param>
|
||||
/// <returns></returns>
|
||||
private IList<HitSampleInfo> sampleInfoListAt(double time)
|
||||
{
|
||||
if (!(HitObject is IHasCurve curveData))
|
||||
return HitObject.Samples;
|
||||
|
||||
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
|
||||
|
||||
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
|
||||
return curveData.NodeSamples[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -505,16 +505,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
}
|
||||
else
|
||||
{
|
||||
var holdNote = new HoldNote
|
||||
newObject = new HoldNote
|
||||
{
|
||||
StartTime = startTime,
|
||||
Column = column,
|
||||
Duration = endTime - startTime,
|
||||
Head = { Samples = sampleInfoListAt(startTime) },
|
||||
Tail = { Samples = sampleInfoListAt(endTime) }
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
};
|
||||
|
||||
newObject = holdNote;
|
||||
}
|
||||
|
||||
pattern.Add(newObject);
|
||||
|
@ -64,21 +64,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
if (holdNote)
|
||||
{
|
||||
var hold = new HoldNote
|
||||
newObject = new HoldNote
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTime - HitObject.StartTime,
|
||||
Column = column,
|
||||
Duration = endTime - HitObject.StartTime
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
};
|
||||
|
||||
if (hold.Head.Samples == null)
|
||||
hold.Head.Samples = new List<HitSampleInfo>();
|
||||
|
||||
hold.Head.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
|
||||
|
||||
hold.Tail.Samples = HitObject.Samples;
|
||||
|
||||
newObject = hold;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
64
osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class DualStageVariantGenerator
|
||||
{
|
||||
private readonly int singleStageVariant;
|
||||
private readonly InputKey[] stage1LeftKeys;
|
||||
private readonly InputKey[] stage1RightKeys;
|
||||
private readonly InputKey[] stage2LeftKeys;
|
||||
private readonly InputKey[] stage2RightKeys;
|
||||
|
||||
public DualStageVariantGenerator(int singleStageVariant)
|
||||
{
|
||||
this.singleStageVariant = singleStageVariant;
|
||||
|
||||
// 10K is special because it expands towards the centre of the keyboard (VM/BN), rather than towards the edges of the keyboard.
|
||||
if (singleStageVariant == 10)
|
||||
{
|
||||
stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.V };
|
||||
stage1RightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
|
||||
|
||||
stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B };
|
||||
stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
|
||||
}
|
||||
else
|
||||
{
|
||||
stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R };
|
||||
stage1RightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
|
||||
|
||||
stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G };
|
||||
stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<KeyBinding> GenerateMappings()
|
||||
{
|
||||
var stage1Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = stage1LeftKeys,
|
||||
RightKeys = stage1RightKeys,
|
||||
SpecialKey = InputKey.V,
|
||||
SpecialAction = ManiaAction.Special1,
|
||||
NormalActionStart = ManiaAction.Key1
|
||||
}.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
|
||||
|
||||
var stage2Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = stage2LeftKeys,
|
||||
RightKeys = stage2RightKeys,
|
||||
SpecialKey = InputKey.B,
|
||||
SpecialAction = ManiaAction.Special2,
|
||||
NormalActionStart = nextNormal
|
||||
}.GenerateKeyBindingsFor(singleStageVariant, out _);
|
||||
|
||||
return stage1Bindings.Concat(stage2Bindings);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osuTK;
|
||||
@ -46,6 +47,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
bodyPiece.Height = (bottomPosition - topPosition).Y;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
EndPlacement(true);
|
||||
}
|
||||
|
||||
private double originalStartTime;
|
||||
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition)
|
||||
|
@ -50,16 +50,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
|
||||
HitObject.Column = Column.Index;
|
||||
BeginPlacement(TimeAt(e.ScreenSpaceMousePosition));
|
||||
BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
EndPlacement(true);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
if (!PlacementActive)
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
|
||||
@ -26,5 +27,15 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
Width = SnappedWidth;
|
||||
Position = SnappedMousePosition;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
base.OnMouseDown(e);
|
||||
|
||||
// Place the note immediately.
|
||||
EndPlacement(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,5 +78,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
[Description("Key 18")]
|
||||
Key18,
|
||||
|
||||
[Description("Key 19")]
|
||||
Key19,
|
||||
|
||||
[Description("Key 20")]
|
||||
Key20,
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaRuleset : Ruleset, ILegacyRuleset
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of supported keys in a single stage.
|
||||
/// </summary>
|
||||
public const int MAX_STAGE_KEYS = 10;
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||
@ -202,6 +207,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ManiaModKey7(),
|
||||
new ManiaModKey8(),
|
||||
new ManiaModKey9(),
|
||||
new ManiaModKey10(),
|
||||
new ManiaModKey1(),
|
||||
new ManiaModKey2(),
|
||||
new ManiaModKey3()),
|
||||
@ -250,9 +256,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 1; i <= 9; i++)
|
||||
for (int i = 1; i <= MAX_STAGE_KEYS; i++)
|
||||
yield return (int)PlayfieldType.Single + i;
|
||||
for (int i = 2; i <= 18; i += 2)
|
||||
for (int i = 2; i <= MAX_STAGE_KEYS * 2; i += 2)
|
||||
yield return (int)PlayfieldType.Dual + i;
|
||||
}
|
||||
}
|
||||
@ -262,73 +268,10 @@ namespace osu.Game.Rulesets.Mania
|
||||
switch (getPlayfieldType(variant))
|
||||
{
|
||||
case PlayfieldType.Single:
|
||||
return new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = new[]
|
||||
{
|
||||
InputKey.A,
|
||||
InputKey.S,
|
||||
InputKey.D,
|
||||
InputKey.F
|
||||
},
|
||||
RightKeys = new[]
|
||||
{
|
||||
InputKey.J,
|
||||
InputKey.K,
|
||||
InputKey.L,
|
||||
InputKey.Semicolon
|
||||
},
|
||||
SpecialKey = InputKey.Space,
|
||||
SpecialAction = ManiaAction.Special1,
|
||||
NormalActionStart = ManiaAction.Key1,
|
||||
}.GenerateKeyBindingsFor(variant, out _);
|
||||
return new SingleStageVariantGenerator(variant).GenerateMappings();
|
||||
|
||||
case PlayfieldType.Dual:
|
||||
int keys = getDualStageKeyCount(variant);
|
||||
|
||||
var stage1Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = new[]
|
||||
{
|
||||
InputKey.Q,
|
||||
InputKey.W,
|
||||
InputKey.E,
|
||||
InputKey.R,
|
||||
},
|
||||
RightKeys = new[]
|
||||
{
|
||||
InputKey.X,
|
||||
InputKey.C,
|
||||
InputKey.V,
|
||||
InputKey.B
|
||||
},
|
||||
SpecialKey = InputKey.S,
|
||||
SpecialAction = ManiaAction.Special1,
|
||||
NormalActionStart = ManiaAction.Key1
|
||||
}.GenerateKeyBindingsFor(keys, out var nextNormal);
|
||||
|
||||
var stage2Bindings = new VariantMappingGenerator
|
||||
{
|
||||
LeftKeys = new[]
|
||||
{
|
||||
InputKey.Number7,
|
||||
InputKey.Number8,
|
||||
InputKey.Number9,
|
||||
InputKey.Number0
|
||||
},
|
||||
RightKeys = new[]
|
||||
{
|
||||
InputKey.K,
|
||||
InputKey.L,
|
||||
InputKey.Semicolon,
|
||||
InputKey.Quote
|
||||
},
|
||||
SpecialKey = InputKey.I,
|
||||
SpecialAction = ManiaAction.Special2,
|
||||
NormalActionStart = nextNormal
|
||||
}.GenerateKeyBindingsFor(keys, out _);
|
||||
|
||||
return stage1Bindings.Concat(stage2Bindings);
|
||||
return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings();
|
||||
}
|
||||
|
||||
return Array.Empty<KeyBinding>();
|
||||
@ -364,59 +307,6 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
|
||||
}
|
||||
|
||||
private class VariantMappingGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the left hand.
|
||||
/// </summary>
|
||||
public InputKey[] LeftKeys;
|
||||
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the right hand.
|
||||
/// </summary>
|
||||
public InputKey[] RightKeys;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InputKey"/> for the special key.
|
||||
/// </summary>
|
||||
public InputKey SpecialKey;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
|
||||
/// </summary>
|
||||
public ManiaAction NormalActionStart;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> for the special column.
|
||||
/// </summary>
|
||||
public ManiaAction SpecialAction;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
|
||||
/// </summary>
|
||||
/// <param name="columns">The number of columns that need to be bound.</param>
|
||||
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
|
||||
/// <returns>The keybindings.</returns>
|
||||
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
|
||||
{
|
||||
ManiaAction currentNormalAction = NormalActionStart;
|
||||
|
||||
var bindings = new List<KeyBinding>();
|
||||
|
||||
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
|
||||
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
|
||||
|
||||
if (columns % 2 == 1)
|
||||
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
|
||||
|
||||
for (int i = 0; i < columns / 2; i++)
|
||||
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
|
||||
|
||||
nextNormalAction = currentNormalAction;
|
||||
return bindings;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
13
osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModKey10 : ManiaKeyMod
|
||||
{
|
||||
public override int KeyCount => 10;
|
||||
public override string Name => "Ten Keys";
|
||||
public override string Acronym => "10K";
|
||||
public override string Description => @"Play with ten keys.";
|
||||
}
|
||||
}
|
@ -51,7 +51,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece())
|
||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
})
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
},
|
||||
@ -127,6 +130,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
public override void PlaySamples()
|
||||
{
|
||||
// Samples are played by the head/tail notes.
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
||||
|
||||
public DefaultBodyPiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Blending = BlendingParameters.Additive;
|
||||
|
||||
AddLayout(subtractionCache);
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -28,7 +30,9 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
set
|
||||
{
|
||||
duration = value;
|
||||
Tail.StartTime = EndTime;
|
||||
|
||||
if (Tail != null)
|
||||
Tail.StartTime = EndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,8 +42,12 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
set
|
||||
{
|
||||
base.StartTime = value;
|
||||
Head.StartTime = value;
|
||||
Tail.StartTime = EndTime;
|
||||
|
||||
if (Head != null)
|
||||
Head.StartTime = value;
|
||||
|
||||
if (Tail != null)
|
||||
Tail.StartTime = EndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,20 +57,26 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
set
|
||||
{
|
||||
base.Column = value;
|
||||
Head.Column = value;
|
||||
Tail.Column = value;
|
||||
|
||||
if (Head != null)
|
||||
Head.Column = value;
|
||||
|
||||
if (Tail != null)
|
||||
Tail.Column = value;
|
||||
}
|
||||
}
|
||||
|
||||
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The head note of the hold.
|
||||
/// </summary>
|
||||
public readonly Note Head = new Note();
|
||||
public Note Head { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tail note of the hold.
|
||||
/// </summary>
|
||||
public readonly TailNote Tail = new TailNote();
|
||||
public TailNote Tail { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between ticks of this hold.
|
||||
@ -83,8 +97,19 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
|
||||
createTicks();
|
||||
|
||||
AddNested(Head);
|
||||
AddNested(Tail);
|
||||
AddNested(Head = new Note
|
||||
{
|
||||
StartTime = StartTime,
|
||||
Column = Column,
|
||||
Samples = getNodeSamples(0),
|
||||
});
|
||||
|
||||
AddNested(Tail = new TailNote
|
||||
{
|
||||
StartTime = EndTime,
|
||||
Column = Column,
|
||||
Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
||||
});
|
||||
}
|
||||
|
||||
private void createTicks()
|
||||
@ -105,5 +130,8 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
public override Judgement CreateJudgement() => new IgnoreJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
|
||||
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Mania.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public abstract class ManiaHitObject : HitObject, IHasColumn
|
||||
public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
|
||||
{
|
||||
public readonly Bindable<int> ColumnBindable = new Bindable<int>();
|
||||
|
||||
@ -20,5 +21,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
||||
float IHasXPosition.X => Column;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
while (activeColumns > 0)
|
||||
{
|
||||
var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter);
|
||||
bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
|
||||
|
||||
if ((activeColumns & 1) > 0)
|
||||
Actions.Add(isSpecial ? specialAction : normalAction);
|
||||
@ -58,33 +58,87 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
int keys = 0;
|
||||
|
||||
var specialColumns = new List<int>();
|
||||
|
||||
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
|
||||
{
|
||||
if (maniaBeatmap.Stages.First().IsSpecialColumn(i))
|
||||
specialColumns.Add(i);
|
||||
}
|
||||
|
||||
foreach (var action in Actions)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ManiaAction.Special1:
|
||||
keys |= 1 << specialColumns[0];
|
||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
|
||||
break;
|
||||
|
||||
case ManiaAction.Special2:
|
||||
keys |= 1 << specialColumns[1];
|
||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
keys |= 1 << (action - ManiaAction.Key1);
|
||||
// the index in lazer, which doesn't include special keys.
|
||||
int nonSpecialKeyIndex = action - ManiaAction.Key1;
|
||||
|
||||
// the index inclusive of special keys.
|
||||
int overallIndex = 0;
|
||||
|
||||
// iterate to find the index including special keys.
|
||||
for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
|
||||
{
|
||||
// skip over special columns.
|
||||
if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
|
||||
continue;
|
||||
// found a non-special column to use.
|
||||
if (nonSpecialKeyIndex == 0)
|
||||
break;
|
||||
// found a non-special column but not ours.
|
||||
nonSpecialKeyIndex--;
|
||||
}
|
||||
|
||||
keys |= 1 << overallIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the overall index (across all stages) for a specified special key.
|
||||
/// </summary>
|
||||
/// <param name="maniaBeatmap">The beatmap.</param>
|
||||
/// <param name="specialOffset">The special key offset (0 is S1).</param>
|
||||
/// <returns>The overall index for the special column.</returns>
|
||||
private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
|
||||
{
|
||||
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
|
||||
{
|
||||
if (isColumnAtIndexSpecial(maniaBeatmap, i))
|
||||
{
|
||||
if (specialOffset == 0)
|
||||
return i;
|
||||
|
||||
specialOffset--;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the column at an overall index (across all stages) is a special column.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap.</param>
|
||||
/// <param name="index">The overall index to check.</param>
|
||||
private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
|
||||
{
|
||||
foreach (var stage in beatmap.Stages)
|
||||
{
|
||||
if (index >= stage.Columns)
|
||||
{
|
||||
index -= stage.Columns;
|
||||
continue;
|
||||
}
|
||||
|
||||
return stage.IsSpecialColumn(index);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Column index is too high.", nameof(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
41
osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs
Normal file
@ -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 _);
|
||||
}
|
||||
}
|
@ -67,6 +67,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = leftLineWidth,
|
||||
Scale = new Vector2(0.740f, 1),
|
||||
Colour = lineColour,
|
||||
Alpha = hasLeftLine ? 1 : 0
|
||||
},
|
||||
@ -76,6 +77,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = rightLineWidth,
|
||||
Scale = new Vector2(0.740f, 1),
|
||||
Colour = lineColour,
|
||||
Alpha = hasRightLine ? 1 : 0
|
||||
},
|
||||
|
61
osu.Game.Rulesets.Mania/VariantMappingGenerator.cs
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class VariantMappingGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the left hand.
|
||||
/// </summary>
|
||||
public InputKey[] LeftKeys;
|
||||
|
||||
/// <summary>
|
||||
/// All the <see cref="InputKey"/>s available to the right hand.
|
||||
/// </summary>
|
||||
public InputKey[] RightKeys;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InputKey"/> for the special key.
|
||||
/// </summary>
|
||||
public InputKey SpecialKey;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
|
||||
/// </summary>
|
||||
public ManiaAction NormalActionStart;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ManiaAction"/> for the special column.
|
||||
/// </summary>
|
||||
public ManiaAction SpecialAction;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
|
||||
/// </summary>
|
||||
/// <param name="columns">The number of columns that need to be bound.</param>
|
||||
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
|
||||
/// <returns>The keybindings.</returns>
|
||||
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
|
||||
{
|
||||
ManiaAction currentNormalAction = NormalActionStart;
|
||||
|
||||
var bindings = new List<KeyBinding>();
|
||||
|
||||
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
|
||||
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
|
||||
|
||||
if (columns % 2 == 1)
|
||||
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
|
||||
|
||||
for (int i = 0; i < columns / 2; i++)
|
||||
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
|
||||
|
||||
nextNormalAction = currentNormalAction;
|
||||
return bindings;
|
||||
}
|
||||
}
|
||||
}
|
106
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -296,6 +296,44 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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}",
|
||||
@ -316,7 +354,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
private List<JudgementResult> judgementResults;
|
||||
private bool allJudgedFired;
|
||||
|
||||
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
|
||||
{
|
||||
@ -342,20 +379,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
p.ScoreProcessor.AllJudged += () =>
|
||||
{
|
||||
if (currentPlayer == p) allJudgedFired = true;
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
allJudgedFired = false;
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for all judged", () => allJudgedFired);
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private class TestHitCircle : HitCircle
|
||||
@ -371,6 +403,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
HeadCircle.HitWindows = new TestHitWindows();
|
||||
TailCircle.HitWindows = new TestHitWindows();
|
||||
|
||||
HeadCircle.HitWindows.SetDifficulty(0);
|
||||
TailCircle.HitWindows.SetDifficulty(0);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private const double time_slider_end = 4000;
|
||||
|
||||
private List<JudgementResult> judgementResults;
|
||||
private bool allJudgedFired;
|
||||
|
||||
/// <summary>
|
||||
/// Scenario:
|
||||
@ -375,20 +374,15 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
p.ScoreProcessor.AllJudged += () =>
|
||||
{
|
||||
if (currentPlayer == p) allJudgedFired = true;
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
allJudgedFired = false;
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for all judged", () => allJudgedFired);
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
|
@ -259,6 +259,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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)
|
||||
|
@ -6,6 +6,7 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
{
|
||||
@ -28,16 +29,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
circlePiece.UpdateFrom(HitObject);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
BeginPlacement();
|
||||
HitObject.Position = ToLocalSpace(screenSpacePosition);
|
||||
}
|
||||
public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
|
||||
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
private readonly Slider slider;
|
||||
@ -146,6 +145,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (RequestSelection == null)
|
||||
return false;
|
||||
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
@ -82,8 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
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,9 +94,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
break;
|
||||
|
||||
case PlacementState.Body:
|
||||
if (e.Button != MouseButton.Left)
|
||||
break;
|
||||
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
{
|
||||
// Place a new point by detatching the current cursor.
|
||||
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
currentSegmentLength = 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
private const double fade_in_duration_multiplier = 0.4;
|
||||
private const double fade_out_duration_multiplier = 0.3;
|
||||
|
||||
protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner);
|
||||
|
||||
public override void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
{
|
||||
static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;
|
||||
|
@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using static osu.Game.Input.Handlers.ReplayInputHandler;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
@ -33,15 +34,30 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
private ReplayState<OsuAction> state;
|
||||
private double lastStateChangeTime;
|
||||
|
||||
private bool hasReplay;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// grab the input manager for future use.
|
||||
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
if (osuInputManager.ReplayInputHandler != null)
|
||||
{
|
||||
hasReplay = true;
|
||||
return;
|
||||
}
|
||||
|
||||
osuInputManager.AllowUserPresses = false;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (hasReplay)
|
||||
return;
|
||||
|
||||
bool requiresHold = false;
|
||||
bool requiresHit = false;
|
||||
|
||||
|
@ -125,7 +125,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return new DrawableSliderTail(slider, tail);
|
||||
|
||||
case SliderHeadCircle head:
|
||||
return new DrawableSliderHead(slider, head) { OnShake = Shake };
|
||||
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 };
|
||||
|
@ -1,16 +1,17 @@
|
||||
// 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;
|
||||
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.
|
||||
/// 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>
|
||||
@ -36,13 +37,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
DrawableHitObject blockingObject = null;
|
||||
|
||||
// Find the last hitobject which blocks future hits.
|
||||
foreach (var obj in hitObjectContainer.AliveObjects)
|
||||
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
||||
{
|
||||
if (obj == hitObject)
|
||||
break;
|
||||
|
||||
if (drawableCanBlockFutureHits(obj))
|
||||
if (hitObjectCanBlockFutureHits(obj))
|
||||
blockingObject = obj;
|
||||
}
|
||||
|
||||
@ -54,74 +51,56 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
// 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).
|
||||
if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
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(HitObject hitObject)
|
||||
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;
|
||||
|
||||
double maximumTime = hitObject.StartTime;
|
||||
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
|
||||
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
||||
|
||||
// Iterate through and apply miss results to all top-level and nested hitobjects which block future hits.
|
||||
foreach (var obj in hitObjectContainer.AliveObjects)
|
||||
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
||||
{
|
||||
if (obj.Judged || obj.HitObject.StartTime >= maximumTime)
|
||||
if (obj.Judged)
|
||||
continue;
|
||||
|
||||
if (hitObjectCanBlockFutureHits(obj.HitObject))
|
||||
applyMiss(obj);
|
||||
|
||||
foreach (var nested in obj.NestedHitObjects)
|
||||
{
|
||||
if (nested.Judged || nested.HitObject.StartTime >= maximumTime)
|
||||
continue;
|
||||
|
||||
if (hitObjectCanBlockFutureHits(nested.HitObject))
|
||||
applyMiss(nested);
|
||||
}
|
||||
if (hitObjectCanBlockFutureHits(obj))
|
||||
((DrawableOsuHitObject)obj).MissForcefully();
|
||||
}
|
||||
|
||||
static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="DrawableHitObject"/> blocks hits on future <see cref="DrawableHitObject"/>s until its start time is reached.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will ONLY match on top-most <see cref="DrawableHitObject"/>s.
|
||||
/// </remarks>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to test.</param>
|
||||
private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject)
|
||||
{
|
||||
// Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over.
|
||||
return hitObject is DrawableHitCircle || hitObject is DrawableSlider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitObject"/> blocks hits on future <see cref="HitObject"/>s until its start time is reached.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is more rigorous and may not match on top-most <see cref="HitObject"/>s as <see cref="drawableCanBlockFutureHits"/> does.
|
||||
/// </remarks>
|
||||
/// <param name="hitObject">The <see cref="HitObject"/> to test.</param>
|
||||
private static bool hitObjectCanBlockFutureHits(HitObject hitObject)
|
||||
{
|
||||
// Unlike the above we will receive slider tails, but they do not block future hits.
|
||||
if (hitObject is SliderTailCircle)
|
||||
return false;
|
||||
private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject)
|
||||
=> hitObject is DrawableHitCircle;
|
||||
|
||||
// All other hitcircles continue to block future hits.
|
||||
return hitObject is HitCircle;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
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(result.HitObject);
|
||||
hitPolicy.HandleHit(judgedObject);
|
||||
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 12 KiB |
BIN
osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png
Executable file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 12 KiB |
@ -6,7 +6,7 @@ using System.Collections.Generic;
|
||||
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
|
||||
{
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ 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
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneDrawableHit : TaikoSkinnableTestScene
|
||||
@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
typeof(DrawableCentreHit),
|
||||
typeof(DrawableRimHit),
|
||||
typeof(LegacyHit),
|
||||
typeof(LegacyCirclePiece),
|
||||
}).ToList();
|
||||
|
||||
[BackgroundDependencyLoader]
|
@ -6,14 +6,14 @@ using System.Collections.Generic;
|
||||
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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ using osuTK;
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneTaikoPlayfield : OsuTestScene
|
||||
public class TestSceneHits : OsuTestScene
|
||||
{
|
||||
private const double default_duration = 1000;
|
||||
private const float scroll_time = 1000;
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
[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));
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
/// 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 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
// 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 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
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;
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit),
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit),
|
||||
_ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ using osu.Framework.Graphics.Containers;
|
||||
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,25 +31,29 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
/// </summary>
|
||||
private int rollingHits;
|
||||
|
||||
private readonly Container<DrawableDrumRollTick> tickContainer;
|
||||
private Container tickContainer;
|
||||
|
||||
private Color4 colourIdle;
|
||||
private Color4 colourEngaged;
|
||||
|
||||
private ElongatedCirclePiece elongatedPiece;
|
||||
|
||||
public DrawableDrumRoll(DrumRoll drumRoll)
|
||||
: base(drumRoll)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
elongatedPiece.Add(tickContainer = new Container<DrawableDrumRollTick> { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
elongatedPiece.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()
|
||||
@ -86,7 +92,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
}
|
||||
|
||||
protected override CompositeDrawable CreateMainPiece() => elongatedPiece = new ElongatedCirclePiece();
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollBody),
|
||||
_ => new ElongatedCirclePiece());
|
||||
|
||||
public override bool OnPressed(TaikoAction action) => false;
|
||||
|
||||
@ -102,8 +109,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
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 as IHasAccentColour)?.FadeAccent(newColour, 100);
|
||||
updateColour();
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
@ -132,8 +138,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -3,10 +3,10 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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
|
||||
{
|
||||
@ -20,10 +20,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
protected override CompositeDrawable 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)
|
||||
{
|
||||
|
@ -92,8 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
// 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 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
// 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;
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit),
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit),
|
||||
_ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ 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
|
||||
{
|
||||
@ -114,12 +115,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
|
||||
}
|
||||
|
||||
protected override CompositeDrawable CreateMainPiece() => new SwellCirclePiece
|
||||
{
|
||||
// to allow for rotation transform
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
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()
|
||||
{
|
||||
@ -184,7 +186,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
.Then()
|
||||
.FadeTo(completion / 8, 2000, Easing.OutQuint);
|
||||
|
||||
MainPiece.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);
|
||||
|
||||
|
@ -2,9 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool OnPressed(TaikoAction action) => false;
|
||||
|
||||
protected override CompositeDrawable CreateMainPiece() => new TickPiece();
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
|
||||
_ => new TickPiece());
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
/// <summary>
|
||||
/// Moves <see cref="Content"/> to a layer proxied above the playfield.
|
||||
/// Does nothing is content is already proxied.
|
||||
/// Does nothing if content is already proxied.
|
||||
/// </summary>
|
||||
protected void ProxyContent()
|
||||
{
|
||||
@ -115,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
public new TObject HitObject;
|
||||
|
||||
protected readonly Vector2 BaseSize;
|
||||
protected readonly CompositeDrawable MainPiece;
|
||||
protected readonly SkinnableDrawable MainPiece;
|
||||
|
||||
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
|
||||
|
||||
@ -167,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
// Normal and clap samples are handled by the drum
|
||||
protected override IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
|
||||
|
||||
protected abstract CompositeDrawable CreateMainPiece();
|
||||
protected abstract SkinnableDrawable CreateMainPiece();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongHitObject"/>.
|
||||
|
@ -10,6 +10,7 @@ using osuTK.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
/// for a usage example.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public abstract class CirclePiece : BeatSyncedContainer
|
||||
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
|
||||
{
|
||||
public const float SYMBOL_SIZE = 0.45f;
|
||||
public const float SYMBOL_BORDER = 8;
|
||||
|
@ -1,7 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
{
|
||||
@ -12,18 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
AccentColour = colours.YellowDark;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
var padding = Content.DrawHeight * Content.Width / 2;
|
||||
|
||||
Content.Padding = new MarginPadding
|
||||
{
|
||||
Left = padding,
|
||||
Right = padding,
|
||||
};
|
||||
|
||||
Width = Parent.DrawSize.X + DrawHeight;
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,20 @@
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class DrumRoll : TaikoHitObject, IHasEndTime
|
||||
public class DrumRoll : TaikoHitObject, IHasCurve
|
||||
{
|
||||
/// <summary>
|
||||
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
|
||||
@ -26,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
public double Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Velocity of this <see cref="DrumRoll"/>.
|
||||
/// </summary>
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Numer of ticks per beat length.
|
||||
/// </summary>
|
||||
@ -54,6 +64,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
|
||||
|
||||
double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
|
||||
tickSpacing = timingPoint.BeatLength / TickRate;
|
||||
overallDifficulty = difficulty.OverallDifficulty;
|
||||
@ -93,5 +107,18 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
public override Judgement CreateJudgement() => new TaikoDrumRollJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
||||
double IHasDistance.Distance => Duration * Velocity;
|
||||
|
||||
int IHasRepeats.RepeatCount { get => 0; set { } }
|
||||
|
||||
List<IList<HitSampleInfo>> IHasRepeats.NodeSamples => new List<IList<HitSampleInfo>>();
|
||||
|
||||
SliderPath IHasCurve.Path
|
||||
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
96
osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
|
||||
{
|
||||
private Drawable backgroundLayer;
|
||||
|
||||
public LegacyCirclePiece()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
|
||||
{
|
||||
Drawable getDrawableFor(string lookup)
|
||||
{
|
||||
const string normal_hit = "taikohit";
|
||||
const string big_hit = "taikobig";
|
||||
|
||||
string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
|
||||
|
||||
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
|
||||
// fallback to regular size if "big" version doesn't exist.
|
||||
skin.GetAnimation($"{normal_hit}{lookup}", true, false);
|
||||
}
|
||||
|
||||
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
||||
AddInternal(backgroundLayer = getDrawableFor("circle"));
|
||||
|
||||
var foregroundLayer = getDrawableFor("circleoverlay");
|
||||
if (foregroundLayer != null)
|
||||
AddInternal(foregroundLayer);
|
||||
|
||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||
// For now just stop at first frame for sanity.
|
||||
foreach (var c in InternalChildren)
|
||||
{
|
||||
(c as IFramedAnimation)?.Stop();
|
||||
|
||||
c.Anchor = Anchor.Centre;
|
||||
c.Origin = Anchor.Centre;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateAccentColour();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
|
||||
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
|
||||
foreach (var c in InternalChildren)
|
||||
c.Scale = new Vector2(DrawHeight / 128);
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
if (value == accentColour)
|
||||
return;
|
||||
|
||||
accentColour = value;
|
||||
if (IsLoaded)
|
||||
updateAccentColour();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAccentColour()
|
||||
{
|
||||
backgroundLayer.Colour = accentColour;
|
||||
}
|
||||
}
|
||||
}
|
83
osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour
|
||||
{
|
||||
private LegacyCirclePiece headCircle;
|
||||
|
||||
private Sprite body;
|
||||
|
||||
private Sprite end;
|
||||
|
||||
public LegacyDrumRoll()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, OsuColour colours)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
end = new Sprite
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = skin.GetTexture("taiko-roll-end"),
|
||||
FillMode = FillMode.Fit,
|
||||
},
|
||||
body = new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = skin.GetTexture("taiko-roll-middle"),
|
||||
},
|
||||
headCircle = new LegacyCirclePiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
};
|
||||
|
||||
AccentColour = colours.YellowDark;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateAccentColour();
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
if (value == accentColour)
|
||||
return;
|
||||
|
||||
accentColour = value;
|
||||
if (IsLoaded)
|
||||
updateAccentColour();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAccentColour()
|
||||
{
|
||||
headCircle.AccentColour = accentColour;
|
||||
body.Colour = accentColour;
|
||||
end.Colour = accentColour;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,90 +2,25 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
public class LegacyHit : CompositeDrawable, IHasAccentColour
|
||||
public class LegacyHit : LegacyCirclePiece
|
||||
{
|
||||
private readonly TaikoSkinComponents component;
|
||||
|
||||
private Drawable backgroundLayer;
|
||||
|
||||
public LegacyHit(TaikoSkinComponents component)
|
||||
{
|
||||
this.component = component;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
|
||||
private void load()
|
||||
{
|
||||
Drawable getDrawableFor(string lookup)
|
||||
{
|
||||
const string normal_hit = "taikohit";
|
||||
const string big_hit = "taikobig";
|
||||
|
||||
string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
|
||||
|
||||
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
|
||||
// fallback to regular size if "big" version doesn't exist.
|
||||
skin.GetAnimation($"{normal_hit}{lookup}", true, false);
|
||||
}
|
||||
|
||||
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
||||
AddInternal(backgroundLayer = getDrawableFor("circle"));
|
||||
|
||||
var foregroundLayer = getDrawableFor("circleoverlay");
|
||||
if (foregroundLayer != null)
|
||||
AddInternal(foregroundLayer);
|
||||
|
||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||
// For now just stop at first frame for sanity.
|
||||
foreach (var c in InternalChildren)
|
||||
{
|
||||
(c as IFramedAnimation)?.Stop();
|
||||
|
||||
c.Anchor = Anchor.Centre;
|
||||
c.Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
AccentColour = component == TaikoSkinComponents.CentreHit
|
||||
? new Color4(235, 69, 44, 255)
|
||||
: new Color4(67, 142, 172, 255);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
|
||||
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
|
||||
foreach (var c in InternalChildren)
|
||||
c.Scale = new Vector2(DrawWidth / 128);
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
if (value == accentColour)
|
||||
return;
|
||||
|
||||
backgroundLayer.Colour = accentColour = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,36 +20,41 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
private LegacyHalfDrum left;
|
||||
private LegacyHalfDrum right;
|
||||
private Container content;
|
||||
|
||||
public LegacyInputDrum()
|
||||
{
|
||||
Size = new Vector2(180, 200);
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
Child = content = new Container
|
||||
{
|
||||
new Sprite
|
||||
Size = new Vector2(180, 200),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Texture = skin.GetTexture("taiko-bar-left")
|
||||
},
|
||||
left = new LegacyHalfDrum(false)
|
||||
{
|
||||
Name = "Left Half",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RimAction = TaikoAction.LeftRim,
|
||||
CentreAction = TaikoAction.LeftCentre
|
||||
},
|
||||
right = new LegacyHalfDrum(true)
|
||||
{
|
||||
Name = "Right Half",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.TopRight,
|
||||
Scale = new Vector2(-1, 1),
|
||||
RimAction = TaikoAction.RightRim,
|
||||
CentreAction = TaikoAction.RightCentre
|
||||
new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("taiko-bar-left")
|
||||
},
|
||||
left = new LegacyHalfDrum(false)
|
||||
{
|
||||
Name = "Left Half",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RimAction = TaikoAction.LeftRim,
|
||||
CentreAction = TaikoAction.LeftCentre
|
||||
},
|
||||
right = new LegacyHalfDrum(true)
|
||||
{
|
||||
Name = "Right Half",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.TopRight,
|
||||
Scale = new Vector2(-1, 1),
|
||||
RimAction = TaikoAction.RightRim,
|
||||
CentreAction = TaikoAction.RightCentre
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -60,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
const float ratio = 1.6f;
|
||||
|
||||
// because the right half is flipped, we need to position using width - position to get the true "topleft" origin position
|
||||
float negativeScaleAdjust = Width / ratio;
|
||||
float negativeScaleAdjust = content.Width / ratio;
|
||||
|
||||
if (skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
|
||||
{
|
||||
@ -78,6 +83,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
|
||||
// This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
|
||||
content.Scale = new Vector2(DrawHeight / content.Size.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A half-drum. Contains one centre and one rim hit.
|
||||
/// </summary>
|
||||
|
59
osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs
Normal file
@ -0,0 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
public class TaikoLegacyHitTarget : CompositeDrawable
|
||||
{
|
||||
private Container content;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("approachcircle"),
|
||||
Scale = new Vector2(0.73f),
|
||||
Alpha = 0.7f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("taikobigcircle"),
|
||||
Scale = new Vector2(0.7f),
|
||||
Alpha = 0.5f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
|
||||
// This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
|
||||
content.Scale = new Vector2(DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
{
|
||||
@ -27,6 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
|
||||
switch (taikoComponent.Component)
|
||||
{
|
||||
case TaikoSkinComponents.DrumRollBody:
|
||||
if (GetTexture("taiko-roll-middle") != null)
|
||||
return new LegacyDrumRoll();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.InputDrum:
|
||||
if (GetTexture("taiko-bar-left") != null)
|
||||
return new LegacyInputDrum();
|
||||
@ -40,6 +47,34 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
return new LegacyHit(taikoComponent.Component);
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.DrumRollTick:
|
||||
return this.GetAnimation("sliderscorepoint", false, false);
|
||||
|
||||
case TaikoSkinComponents.HitTarget:
|
||||
if (GetTexture("taikobigcircle") != null)
|
||||
return new TaikoLegacyHitTarget();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.PlayfieldBackgroundRight:
|
||||
if (GetTexture("taiko-bar-right") != null)
|
||||
{
|
||||
return this.GetAnimation("taiko-bar-right", false, false).With(d =>
|
||||
{
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
d.Size = Vector2.One;
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.PlayfieldBackgroundLeft:
|
||||
// This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins).
|
||||
if (GetTexture("taiko-bar-right") != null)
|
||||
return Drawable.Empty();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.GetDrawableComponent(component);
|
||||
|
@ -7,6 +7,12 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
InputDrum,
|
||||
CentreHit,
|
||||
RimHit
|
||||
RimHit,
|
||||
DrumRollBody,
|
||||
DrumRollTick,
|
||||
Swell,
|
||||
HitTarget,
|
||||
PlayfieldBackgroundLeft,
|
||||
PlayfieldBackgroundRight
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
JudgedObject = judgedObject;
|
||||
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
sampleMapping = new DrumSampleMapping(controlPoints);
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
FillMode = FillMode.Fit;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -40,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Scale = new Vector2(0.9f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TaikoHalfDrum(false)
|
||||
|
37
osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
internal class PlayfieldBackgroundLeft : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.Gray1,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 10,
|
||||
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public class PlayfieldBackgroundRight : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Name = "Transparent playfield background";
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
BorderColour = colours.Gray1;
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.2f),
|
||||
Radius = 5,
|
||||
};
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.Gray0,
|
||||
Alpha = 0.6f
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Border",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
MaskingSmoothness = 0,
|
||||
BorderThickness = 2,
|
||||
AlwaysPresent = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -13,15 +13,17 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
/// <summary>
|
||||
/// A component that is displayed at the hit position in the taiko playfield.
|
||||
/// </summary>
|
||||
internal class HitTarget : Container
|
||||
internal class TaikoHitTarget : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// Thickness of all drawn line pieces.
|
||||
/// </summary>
|
||||
private const float border_thickness = 2.5f;
|
||||
|
||||
public HitTarget()
|
||||
public TaikoHitTarget()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -39,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
@ -61,7 +62,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE),
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
@ -3,11 +3,8 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -17,169 +14,113 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public class TaikoPlayfield : ScrollingPlayfield
|
||||
{
|
||||
private readonly ControlPointInfo controlPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
|
||||
/// </summary>
|
||||
public const float DEFAULT_HEIGHT = 178;
|
||||
|
||||
/// <summary>
|
||||
/// The offset from <see cref="left_area_size"/> which the center of the hit target lies at.
|
||||
/// </summary>
|
||||
public const float HIT_TARGET_OFFSET = 100;
|
||||
private Container<HitExplosion> hitExplosionContainer;
|
||||
private Container<KiaiHitExplosion> kiaiExplosionContainer;
|
||||
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
||||
internal Drawable HitTarget;
|
||||
|
||||
/// <summary>
|
||||
/// The size of the left area of the playfield. This area contains the input drum.
|
||||
/// </summary>
|
||||
private const float left_area_size = 240;
|
||||
private ProxyContainer topLevelHitContainer;
|
||||
private ProxyContainer barlineContainer;
|
||||
private Container rightArea;
|
||||
private Container leftArea;
|
||||
|
||||
private readonly Container<HitExplosion> hitExplosionContainer;
|
||||
private readonly Container<KiaiHitExplosion> kiaiExplosionContainer;
|
||||
private readonly JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
||||
internal readonly HitTarget HitTarget;
|
||||
|
||||
private readonly ProxyContainer topLevelHitContainer;
|
||||
private readonly ProxyContainer barlineContainer;
|
||||
|
||||
private readonly Container overlayBackgroundContainer;
|
||||
private readonly Container backgroundContainer;
|
||||
|
||||
private readonly Box overlayBackground;
|
||||
private readonly Box background;
|
||||
private Container hitTargetOffsetContent;
|
||||
|
||||
public TaikoPlayfield(ControlPointInfo controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
backgroundContainer = new Container
|
||||
{
|
||||
Name = "Transparent playfield background",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.2f),
|
||||
Radius = 5,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.6f
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()),
|
||||
rightArea = new Container
|
||||
{
|
||||
Name = "Right area",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = left_area_size },
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Name = "Masked elements before hit objects",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
FillMode = FillMode.Fit,
|
||||
Children = new[]
|
||||
{
|
||||
hitExplosionContainer = new Container<HitExplosion>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
HitTarget = new HitTarget
|
||||
HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget())
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit
|
||||
}
|
||||
}
|
||||
},
|
||||
barlineContainer = new ProxyContainer
|
||||
hitTargetOffsetContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Hit objects",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
|
||||
Masking = true,
|
||||
Child = HitObjectContainer
|
||||
},
|
||||
kiaiExplosionContainer = new Container<KiaiHitExplosion>
|
||||
{
|
||||
Name = "Kiai hit explosions",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
|
||||
Blending = BlendingParameters.Additive
|
||||
},
|
||||
judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
|
||||
{
|
||||
Name = "Judgements",
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
|
||||
Blending = BlendingParameters.Additive
|
||||
Children = new Drawable[]
|
||||
{
|
||||
barlineContainer = new ProxyContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Hit objects",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = HitObjectContainer
|
||||
},
|
||||
kiaiExplosionContainer = new Container<KiaiHitExplosion>
|
||||
{
|
||||
Name = "Kiai hit explosions",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Blending = BlendingParameters.Additive
|
||||
},
|
||||
judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
|
||||
{
|
||||
Name = "Judgements",
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Blending = BlendingParameters.Additive
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
overlayBackgroundContainer = new Container
|
||||
leftArea = new Container
|
||||
{
|
||||
Name = "Left overlay",
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Size = new Vector2(left_area_size, 1),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
BorderColour = colours.Gray0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
overlayBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
|
||||
new InputDrum(controlPoints)
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Scale = new Vector2(0.9f),
|
||||
Margin = new MarginPadding { Right = 20 }
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 10,
|
||||
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Border",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
MaskingSmoothness = 0,
|
||||
BorderThickness = 2,
|
||||
AlwaysPresent = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
},
|
||||
topLevelHitContainer = new ProxyContainer
|
||||
@ -190,14 +131,14 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
protected override void Update()
|
||||
{
|
||||
overlayBackgroundContainer.BorderColour = colours.Gray0;
|
||||
overlayBackground.Colour = colours.Gray1;
|
||||
base.Update();
|
||||
|
||||
backgroundContainer.BorderColour = colours.Gray1;
|
||||
background.Colour = colours.Gray0;
|
||||
// Padding is required to be updated for elements which are based on "absolute" X sized elements.
|
||||
// This is basically allowing for correct alignment as relative pieces move around them.
|
||||
rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
|
||||
hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
|
||||
}
|
||||
|
||||
public override void Add(DrawableHitObject h)
|
||||
|
@ -241,6 +241,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
var controlPoints = decoder.Decode(stream).ControlPointInfo;
|
||||
|
||||
Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4));
|
||||
Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3));
|
||||
Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3));
|
||||
Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3));
|
||||
|
||||
Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
|
||||
Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
|
||||
Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1));
|
||||
|
@ -1,14 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Serialization;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
@ -16,39 +25,91 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
[TestFixture]
|
||||
public class LegacyBeatmapEncoderTest
|
||||
{
|
||||
private const string normal = "Soleily - Renatus (Gamu) [Insane].osu";
|
||||
|
||||
private static IEnumerable<string> allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu"));
|
||||
|
||||
[TestCaseSource(nameof(allBeatmaps))]
|
||||
public void TestDecodeEncodedBeatmap(string name)
|
||||
public void TestBeatmap(string name)
|
||||
{
|
||||
var decoded = decode(normal, out var encoded);
|
||||
var decoded = decode(name, out var encoded);
|
||||
|
||||
sort(decoded);
|
||||
sort(encoded);
|
||||
|
||||
Assert.That(decoded.HitObjects.Count, Is.EqualTo(encoded.HitObjects.Count));
|
||||
Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize()));
|
||||
}
|
||||
|
||||
private Beatmap decode(string filename, out Beatmap encoded)
|
||||
private void sort(IBeatmap beatmap)
|
||||
{
|
||||
using (var stream = TestResources.OpenResource(filename))
|
||||
// Sort control points to ensure a sane ordering, as they may be parsed in different orders. This works because each group contains only uniquely-typed control points.
|
||||
foreach (var g in beatmap.ControlPointInfo.Groups)
|
||||
{
|
||||
ArrayList.Adapter((IList)g.ControlPoints).Sort(
|
||||
Comparer<ControlPoint>.Create((c1, c2) => string.Compare(c1.GetType().ToString(), c2.GetType().ToString(), StringComparison.Ordinal)));
|
||||
}
|
||||
}
|
||||
|
||||
private IBeatmap decode(string filename, out IBeatmap encoded)
|
||||
{
|
||||
using (var stream = TestResources.GetStore().GetStream(filename))
|
||||
using (var sr = new LineBufferedReader(stream))
|
||||
{
|
||||
var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr);
|
||||
var legacyDecoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr));
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
using (var sw = new StreamWriter(ms))
|
||||
using (var sr2 = new LineBufferedReader(ms))
|
||||
using (var sr2 = new LineBufferedReader(ms, true))
|
||||
{
|
||||
new LegacyBeatmapEncoder(legacyDecoded).Encode(sw);
|
||||
sw.Flush();
|
||||
|
||||
sw.Flush();
|
||||
ms.Position = 0;
|
||||
|
||||
encoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2);
|
||||
encoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2));
|
||||
|
||||
return legacyDecoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IBeatmap convert(IBeatmap beatmap)
|
||||
{
|
||||
switch (beatmap.BeatmapInfo.RulesetID)
|
||||
{
|
||||
case 0:
|
||||
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo;
|
||||
break;
|
||||
}
|
||||
|
||||
return new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset);
|
||||
}
|
||||
|
||||
private class TestWorkingBeatmap : WorkingBeatmap
|
||||
{
|
||||
private readonly IBeatmap beatmap;
|
||||
|
||||
public TestWorkingBeatmap(IBeatmap beatmap)
|
||||
: base(beatmap.BeatmapInfo, null)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
|
||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||
|
||||
protected override Track GetTrack() => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
[TestFixture]
|
||||
public class ToStringFormattingTest
|
||||
{
|
||||
[Test]
|
||||
public void TestArtistTitle()
|
||||
{
|
||||
var beatmap = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "artist",
|
||||
Title = "title"
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(beatmap.ToString(), Is.EqualTo("artist - title"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestArtistTitleCreator()
|
||||
{
|
||||
var beatmap = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "artist",
|
||||
Title = "title",
|
||||
Author = new User { Username = "creator" }
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator)"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestArtistTitleCreatorDifficulty()
|
||||
{
|
||||
var beatmap = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "artist",
|
||||
Title = "title",
|
||||
Author = new User { Username = "creator" }
|
||||
},
|
||||
Version = "difficulty"
|
||||
};
|
||||
|
||||
Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator) [difficulty]"));
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Tests.Editor
|
||||
namespace osu.Game.Tests.Editing
|
||||
{
|
||||
[TestFixture]
|
||||
public class EditorChangeHandlerTest
|
||||
@ -15,15 +15,18 @@ namespace osu.Game.Tests.Editor
|
||||
{
|
||||
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.False);
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
Assert.That(handler.CanRedo.Value, Is.False);
|
||||
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.True);
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
Assert.That(handler.CanRedo.Value, Is.False);
|
||||
|
||||
handler.RestoreState(-1);
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.False);
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
Assert.That(handler.CanRedo.Value, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -31,20 +34,20 @@ namespace osu.Game.Tests.Editor
|
||||
{
|
||||
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.False);
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.True);
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
||||
{
|
||||
Assert.That(handler.HasUndoState, Is.True);
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
handler.RestoreState(-1);
|
||||
}
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.False);
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -52,20 +55,20 @@ namespace osu.Game.Tests.Editor
|
||||
{
|
||||
var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()));
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.False);
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++)
|
||||
handler.SaveState();
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.True);
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
|
||||
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
|
||||
{
|
||||
Assert.That(handler.HasUndoState, Is.True);
|
||||
Assert.That(handler.CanUndo.Value, Is.True);
|
||||
handler.RestoreState(-1);
|
||||
}
|
||||
|
||||
Assert.That(handler.HasUndoState, Is.False);
|
||||
Assert.That(handler.CanUndo.Value, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
||||
|
||||
namespace osu.Game.Tests.Editor
|
||||
namespace osu.Game.Tests.Editing
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyEditorBeatmapPatcherTest
|
@ -14,7 +14,7 @@ using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Editor
|
||||
namespace osu.Game.Tests.Editing
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene
|
346
osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
Normal file
@ -0,0 +1,346 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneHitObjectSamples : PlayerTestScene
|
||||
{
|
||||
private readonly SkinInfo userSkinInfo = new SkinInfo();
|
||||
|
||||
private readonly BeatmapInfo beatmapInfo = new BeatmapInfo
|
||||
{
|
||||
BeatmapSet = new BeatmapSetInfo(),
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = User.SYSTEM_USER
|
||||
}
|
||||
};
|
||||
|
||||
private readonly TestResourceStore userSkinResourceStore = new TestResourceStore();
|
||||
private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore();
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
public TestSceneHitObjectSamples()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
private SkinSourceDependencyContainer dependencies;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
=> new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent)));
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDefaultSampleFromUserSkin()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-skin-sample.osu");
|
||||
|
||||
assertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a sample set of 1 retrieves samples from the beatmap skin.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDefaultSampleFromBeatmap()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a sample set of 1 retrieves samples from the user skin when the beatmap does not contain the sample.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDefaultSampleFromUserSkinFallback()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(null, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-sample.osu");
|
||||
|
||||
assertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
|
||||
/// normal-hitnormal2
|
||||
/// normal-hitnormal
|
||||
/// </summary>
|
||||
[TestCase("normal-hitnormal2")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
|
||||
{
|
||||
setupSkins(expectedSample, expectedSample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expectedSample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample:
|
||||
/// normal-hitnormal2
|
||||
/// normal-hitnormal
|
||||
/// </summary>
|
||||
[TestCase("normal-hitnormal2")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
|
||||
{
|
||||
setupSkins(string.Empty, expectedSample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
|
||||
|
||||
assertUserLookup(expectedSample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestFileSampleFromBeatmap()
|
||||
{
|
||||
const string expected_sample = "hit_1.wav";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("file-beatmap-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestControlPointSampleFromSkin()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("controlpoint-skin-sample.osu");
|
||||
|
||||
assertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a control point that provides a custom sample set of 1 causes <see cref="TestDefaultSampleFromBeatmap"/>.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestControlPointSampleFromBeatmap()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("controlpoint-beatmap-sample.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a control point that provides a custom sample of 2 causes <see cref="TestDefaultCustomSampleFromBeatmap"/>.
|
||||
/// </summary>
|
||||
[TestCase("normal-hitnormal2")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
|
||||
{
|
||||
setupSkins(sampleName, sampleName);
|
||||
|
||||
createTestWithBeatmap("controlpoint-beatmap-custom-sample.osu");
|
||||
|
||||
assertBeatmapLookup(sampleName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject's custom sample overrides the control point's.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitObjectCustomSampleOverride()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal3";
|
||||
|
||||
setupSkins(expected_sample, expected_sample);
|
||||
|
||||
createTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu");
|
||||
|
||||
assertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap;
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
=> new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio);
|
||||
|
||||
private IBeatmap currentTestBeatmap;
|
||||
|
||||
private void createTestWithBeatmap(string filename)
|
||||
{
|
||||
CreateTest(() =>
|
||||
{
|
||||
AddStep("clear performed lookups", () =>
|
||||
{
|
||||
userSkinResourceStore.PerformedLookups.Clear();
|
||||
beatmapSkinResourceStore.PerformedLookups.Clear();
|
||||
});
|
||||
|
||||
AddStep($"load {filename}", () =>
|
||||
{
|
||||
using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}")))
|
||||
currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void setupSkins(string beatmapFile, string userFile)
|
||||
{
|
||||
AddStep("setup skins", () =>
|
||||
{
|
||||
userSkinInfo.Files = new List<SkinFileInfo>
|
||||
{
|
||||
new SkinFileInfo
|
||||
{
|
||||
Filename = userFile,
|
||||
FileInfo = new IO.FileInfo { Hash = userFile }
|
||||
}
|
||||
};
|
||||
|
||||
beatmapInfo.BeatmapSet.Files = new List<BeatmapSetFileInfo>
|
||||
{
|
||||
new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = beatmapFile,
|
||||
FileInfo = new IO.FileInfo { Hash = beatmapFile }
|
||||
}
|
||||
};
|
||||
|
||||
// Need to refresh the cached skin source to refresh the skin resource store.
|
||||
dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin",
|
||||
() => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name));
|
||||
|
||||
private void assertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin",
|
||||
() => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name));
|
||||
|
||||
private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer
|
||||
{
|
||||
public ISkinSource SkinSource;
|
||||
|
||||
private readonly IReadOnlyDependencyContainer fallback;
|
||||
|
||||
public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback)
|
||||
{
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
public object Get(Type type)
|
||||
{
|
||||
if (type == typeof(ISkinSource))
|
||||
return SkinSource;
|
||||
|
||||
return fallback.Get(type);
|
||||
}
|
||||
|
||||
public object Get(Type type, CacheInfo info)
|
||||
{
|
||||
if (type == typeof(ISkinSource))
|
||||
return SkinSource;
|
||||
|
||||
return fallback.Get(type, info);
|
||||
}
|
||||
|
||||
public void Inject<T>(T instance) where T : class
|
||||
{
|
||||
// Never used directly
|
||||
}
|
||||
}
|
||||
|
||||
private class TestResourceStore : IResourceStore<byte[]>
|
||||
{
|
||||
public readonly List<string> PerformedLookups = new List<string>();
|
||||
|
||||
public byte[] Get(string name)
|
||||
{
|
||||
markLookup(name);
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
public Task<byte[]> GetAsync(string name)
|
||||
{
|
||||
markLookup(name);
|
||||
return Task.FromResult(Array.Empty<byte>());
|
||||
}
|
||||
|
||||
public Stream GetStream(string name)
|
||||
{
|
||||
markLookup(name);
|
||||
return new MemoryStream();
|
||||
}
|
||||
|
||||
private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1));
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => Enumerable.Empty<string>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap
|
||||
{
|
||||
private readonly BeatmapInfo skinBeatmapInfo;
|
||||
private readonly IResourceStore<byte[]> resourceStore;
|
||||
|
||||
public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio,
|
||||
double length = 60000)
|
||||
: base(beatmap, storyboard, referenceClock, audio, length)
|
||||
{
|
||||
this.skinBeatmapInfo = skinBeatmapInfo;
|
||||
this.resourceStore = resourceStore;
|
||||
}
|
||||
|
||||
protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager);
|
||||
}
|
||||
}
|
||||
}
|
@ -29,11 +29,17 @@ namespace osu.Game.Tests.NonVisual
|
||||
var cpi = new ControlPointInfo();
|
||||
|
||||
cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
|
||||
cpi.Add(1000, new TimingControlPoint()); // is redundant
|
||||
cpi.Add(1000, new TimingControlPoint()); // is also not redundant, due to change of offset
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
|
||||
cpi.Add(1000, new TimingControlPoint()); //is redundant
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -86,11 +92,12 @@ namespace osu.Game.Tests.NonVisual
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
|
||||
|
||||
cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
|
||||
cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant
|
||||
cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,1,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
255,193,2170,1,0,0:0:0:0:hit_1.wav
|
@ -0,0 +1,7 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:3:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:2:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:1:0:
|
@ -0,0 +1,4 @@
|
||||
osu file format v14
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|