mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 02:49:53 +08:00
Compare commits
704 Commits
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ContentModelUserStore">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@@ -17,7 +17,7 @@ The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commo
|
||||
|
||||
This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
|
||||
|
||||
**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
|
||||
**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
|
||||
|
||||
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
|
||||
|
||||
|
||||
+3
-18
@@ -2,13 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.EmptyFreeform.Replays
|
||||
{
|
||||
@@ -21,26 +19,13 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
|
||||
|
||||
protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any();
|
||||
|
||||
protected Vector2 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
var frame = CurrentFrame;
|
||||
|
||||
if (frame == null)
|
||||
return Vector2.Zero;
|
||||
|
||||
Debug.Assert(CurrentTime != null);
|
||||
|
||||
return Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time);
|
||||
}
|
||||
}
|
||||
|
||||
public override void CollectPendingInputs(List<IInput> inputs)
|
||||
{
|
||||
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
|
||||
inputs.Add(new MousePositionAbsoluteInput
|
||||
{
|
||||
Position = GamefieldToScreenSpace(Position),
|
||||
Position = GamefieldToScreenSpace(position),
|
||||
});
|
||||
inputs.Add(new ReplayState<EmptyFreeformAction>
|
||||
{
|
||||
|
||||
+3
-18
@@ -2,12 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Pippidon.Replays
|
||||
{
|
||||
@@ -20,26 +18,13 @@ namespace osu.Game.Rulesets.Pippidon.Replays
|
||||
|
||||
protected override bool IsImportant(PippidonReplayFrame frame) => true;
|
||||
|
||||
protected Vector2 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
var frame = CurrentFrame;
|
||||
|
||||
if (frame == null)
|
||||
return Vector2.Zero;
|
||||
|
||||
Debug.Assert(CurrentTime != null);
|
||||
|
||||
return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CollectPendingInputs(List<IInput> inputs)
|
||||
{
|
||||
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
|
||||
inputs.Add(new MousePositionAbsoluteInput
|
||||
{
|
||||
Position = GamefieldToScreenSpace(Position)
|
||||
Position = GamefieldToScreenSpace(position)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.402.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.424.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -7,6 +7,8 @@ using Android.OS;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
@@ -72,5 +74,14 @@ namespace osu.Android
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
||||
|
||||
private class AndroidBatteryInfo : BatteryInfo
|
||||
{
|
||||
public override double ChargeLevel => Battery.ChargeLevel;
|
||||
|
||||
public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.BATTERY_STATS" />
|
||||
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
|
||||
</manifest>
|
||||
@@ -63,5 +63,8 @@
|
||||
<Version>5.0.0</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
</Project>
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ namespace osu.Desktop
|
||||
/// Allow a maximum of one unhandled exception, per second of execution.
|
||||
/// </summary>
|
||||
/// <param name="arg"></param>
|
||||
/// <returns></returns>
|
||||
private static bool handleException(Exception arg)
|
||||
{
|
||||
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class TestSceneCatchReplay : TestSceneCatchPlayer
|
||||
{
|
||||
protected override bool Autoplay => true;
|
||||
|
||||
private const int object_count = 10;
|
||||
|
||||
[Test]
|
||||
public void TestReplayCatcherPositionIsFramePerfect()
|
||||
{
|
||||
AddUntilStep("caught all fruits", () => Player.ScoreProcessor.Combo.Value == object_count);
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = ruleset,
|
||||
}
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
|
||||
|
||||
for (int i = 0; i < object_count / 2; i++)
|
||||
{
|
||||
beatmap.HitObjects.Add(new Fruit
|
||||
{
|
||||
StartTime = (i + 1) * 1000,
|
||||
X = 0
|
||||
});
|
||||
beatmap.HitObjects.Add(new Fruit
|
||||
{
|
||||
StartTime = (i + 1) * 1000 + 1,
|
||||
X = CatchPlayfield.WIDTH
|
||||
});
|
||||
}
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
@@ -20,6 +21,7 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
@@ -170,16 +172,25 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCatcherStacking()
|
||||
public void TestCatcherRandomStacking()
|
||||
{
|
||||
AddStep("catch more fruits", () => attemptCatch(() => new Fruit
|
||||
{
|
||||
X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(Vector2.One)
|
||||
}, 50));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCatcherStackingSameCaughtPosition()
|
||||
{
|
||||
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
||||
checkPlate(1);
|
||||
AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
|
||||
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
|
||||
checkPlate(10);
|
||||
AddAssert("caught objects are stacked", () =>
|
||||
catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
|
||||
catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
|
||||
catcher.CaughtObjects.Any(obj => obj.Y < -20));
|
||||
catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
|
||||
catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
|
||||
catcher.CaughtObjects.Any(obj => obj.Y < -25));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -189,11 +200,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
||||
AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1);
|
||||
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
|
||||
AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
|
||||
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
|
||||
AddStep("explode", () => catcher.Explode());
|
||||
AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
|
||||
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
|
||||
AddStep("catch fruits", () => attemptCatch(new Fruit(), 10));
|
||||
AddStep("catch fruits", () => attemptCatch(() => new Fruit(), 10));
|
||||
AddStep("drop", () => catcher.Drop());
|
||||
AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
|
||||
}
|
||||
@@ -222,10 +233,15 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
|
||||
|
||||
private void attemptCatch(CatchHitObject hitObject, int count = 1)
|
||||
private void attemptCatch(CatchHitObject hitObject)
|
||||
{
|
||||
attemptCatch(() => hitObject, 1);
|
||||
}
|
||||
|
||||
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
attemptCatch(hitObject, out _, out _);
|
||||
attemptCatch(hitObject(), out _, out _);
|
||||
}
|
||||
|
||||
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
|
||||
|
||||
@@ -8,6 +8,8 @@ using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
@@ -31,12 +33,32 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private float circleSize;
|
||||
|
||||
private ScheduledDelegate addManyFruit;
|
||||
|
||||
private BeatmapDifficulty beatmapDifficulty;
|
||||
|
||||
public TestSceneCatcherArea()
|
||||
{
|
||||
AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
|
||||
AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
|
||||
|
||||
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
||||
AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
|
||||
AddStep("catch many random fruit", () =>
|
||||
{
|
||||
int count = 50;
|
||||
|
||||
addManyFruit?.Cancel();
|
||||
addManyFruit = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
attemptCatch(new Fruit
|
||||
{
|
||||
X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty) * 0.6f,
|
||||
});
|
||||
|
||||
if (count-- == 0)
|
||||
addManyFruit?.Cancel();
|
||||
}, 50, true);
|
||||
});
|
||||
AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true }));
|
||||
AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit()));
|
||||
AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true }));
|
||||
@@ -45,10 +67,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
private void attemptCatch(Fruit fruit)
|
||||
{
|
||||
fruit.X = fruit.OriginalX + catcher.X;
|
||||
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = circleSize
|
||||
});
|
||||
fruit.ApplyDefaults(new ControlPointInfo(), beatmapDifficulty);
|
||||
|
||||
foreach (var area in this.ChildrenOfType<CatcherArea>())
|
||||
{
|
||||
@@ -71,6 +90,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
circleSize = size;
|
||||
|
||||
beatmapDifficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = circleSize
|
||||
};
|
||||
|
||||
SetContents(() =>
|
||||
{
|
||||
var droppedObjectContainer = new Container<CaughtObject>
|
||||
@@ -84,7 +108,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Children = new Drawable[]
|
||||
{
|
||||
droppedObjectContainer,
|
||||
new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size })
|
||||
new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
|
||||
@@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
return new Mod[]
|
||||
{
|
||||
new CatchModDifficultyAdjust(),
|
||||
new CatchModClassic(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
@@ -126,7 +127,8 @@ namespace osu.Game.Rulesets.Catch
|
||||
case ModType.Fun:
|
||||
return new Mod[]
|
||||
{
|
||||
new MultiMod(new ModWindUp(), new ModWindDown())
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new CatchModFloatingFruits()
|
||||
};
|
||||
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModClassic : ModClassic
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModFloatingFruits : Mod, IApplicableToDrawableRuleset<CatchHitObject>
|
||||
{
|
||||
public override string Name => "Floating Fruits";
|
||||
public override string Acronym => "FF";
|
||||
public override string Description => "The fruits are... floating?";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Anchor = Anchor.Centre;
|
||||
drawableRuleset.Origin = Anchor.Centre;
|
||||
|
||||
drawableRuleset.Scale = new Vector2(1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,10 +125,6 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
private void addFrame(double time, float? position = null, bool dashing = false)
|
||||
{
|
||||
// todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
|
||||
if (Replay.Frames.Count == 0)
|
||||
Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null));
|
||||
|
||||
var last = currentFrame;
|
||||
currentFrame = new CatchReplayFrame(time, position, dashing, last);
|
||||
Replay.Frames.Add(currentFrame);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Utils;
|
||||
@@ -20,29 +19,14 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any();
|
||||
|
||||
protected float? Position
|
||||
{
|
||||
get
|
||||
{
|
||||
var frame = CurrentFrame;
|
||||
|
||||
if (frame == null)
|
||||
return null;
|
||||
|
||||
Debug.Assert(CurrentTime != null);
|
||||
|
||||
return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CollectPendingInputs(List<IInput> inputs)
|
||||
{
|
||||
if (!Position.HasValue) return;
|
||||
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
|
||||
inputs.Add(new CatchReplayState
|
||||
{
|
||||
PressedActions = CurrentFrame?.Actions ?? new List<CatchAction>(),
|
||||
CatcherX = Position.Value
|
||||
CatcherX = position
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
droppedObjectContainer,
|
||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
||||
HitObjectContainer,
|
||||
HitObjectContainer.CreateProxy(),
|
||||
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
|
||||
// make sure the up-to-date catcher position is used for the catcher catching logic of hit objects.
|
||||
CatcherArea,
|
||||
HitObjectContainer,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,16 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
public const double BASE_SPEED = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
|
||||
/// </summary>
|
||||
public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
|
||||
|
||||
/// <summary>
|
||||
/// The amount by which caught fruit should be scaled down to fit on the plate.
|
||||
/// </summary>
|
||||
private const float caught_fruit_scale_adjust = 0.5f;
|
||||
|
||||
[NotNull]
|
||||
private readonly Container trailsTarget;
|
||||
|
||||
@@ -202,13 +212,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// 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;
|
||||
public 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));
|
||||
public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
|
||||
|
||||
/// <summary>
|
||||
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.
|
||||
@@ -240,7 +250,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (result.IsHit)
|
||||
{
|
||||
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2);
|
||||
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X);
|
||||
|
||||
if (CatchFruitOnPlate)
|
||||
placeCaughtObject(palpableObject, positionInStack);
|
||||
@@ -384,16 +394,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
updateTrailVisibility();
|
||||
|
||||
if (hyperDashing)
|
||||
{
|
||||
this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||
this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||
this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
this.FadeColour(hyperDashing ? hyperDashColour : Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
|
||||
@@ -479,7 +480,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
caughtObject.CopyStateFrom(drawableObject);
|
||||
caughtObject.Anchor = Anchor.TopCentre;
|
||||
caughtObject.Position = position;
|
||||
caughtObject.Scale /= 2;
|
||||
caughtObject.Scale *= caught_fruit_scale_adjust;
|
||||
|
||||
caughtObjectContainer.Add(caughtObject);
|
||||
|
||||
@@ -489,19 +490,21 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
private Vector2 computePositionInStack(Vector2 position, float displayRadius)
|
||||
{
|
||||
const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2;
|
||||
const float allowance = 10;
|
||||
// this is taken from osu-stable (lenience should be 10 * 10 at standard scale).
|
||||
const float lenience_adjust = 10 / CatchHitObject.OBJECT_RADIUS;
|
||||
|
||||
while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2)))
|
||||
float adjustedRadius = displayRadius * lenience_adjust;
|
||||
float checkDistance = MathF.Pow(adjustedRadius, 2);
|
||||
|
||||
// offset fruit vertically to better place "above" the plate.
|
||||
position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
|
||||
|
||||
while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
|
||||
{
|
||||
float diff = (displayRadius + radius_div_2) / allowance;
|
||||
|
||||
position.X += (RNG.NextSingle() - 0.5f) * diff * 2;
|
||||
position.Y -= RNG.NextSingle() * diff;
|
||||
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
|
||||
position.Y -= RNG.NextSingle(0, 5);
|
||||
}
|
||||
|
||||
position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
/// <summary>
|
||||
/// The number of frames which are generated at the start of a replay regardless of hitobject content.
|
||||
/// </summary>
|
||||
private const int frame_offset = 1;
|
||||
private const int frame_offset = 0;
|
||||
|
||||
[Test]
|
||||
public void TestSingleNote()
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
|
||||
@@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
|
||||
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
|
||||
@@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||
@@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
|
||||
Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
|
||||
@@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
|
||||
Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
|
||||
@@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||
|
||||
Assert.IsTrue(generated.Frames.Count == frame_offset + 3, "Replay must have 3 generated frames");
|
||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 3, "Incorrect number of frames");
|
||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
|
||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
|
||||
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
|
||||
|
||||
@@ -324,6 +324,33 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
assertTailJudgement(HitResult.Ok);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroLength()
|
||||
{
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 0,
|
||||
Column = 0,
|
||||
},
|
||||
},
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1),
|
||||
}, beatmap);
|
||||
|
||||
AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
private void assertHeadJudgement(HitResult result)
|
||||
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result);
|
||||
|
||||
|
||||
@@ -482,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// 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(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
return;
|
||||
|
||||
base.OnMouseUp(e);
|
||||
EndPlacement(true);
|
||||
EndPlacement(HitObject.Duration > 0);
|
||||
}
|
||||
|
||||
private double originalStartTime;
|
||||
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (PlacementActive)
|
||||
if (PlacementActive == PlacementState.Active)
|
||||
{
|
||||
if (result.Time is double endTime)
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (!PlacementActive)
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = result.Playfield as Column;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ManiaModDualStages(),
|
||||
new ManiaModMirror(),
|
||||
new ManiaModDifficultyAdjust(),
|
||||
new ManiaModClassic(),
|
||||
new ManiaModInvert(),
|
||||
new ManiaModConstantSpeed()
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModClassic : ModClassic
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Name => "Mirror";
|
||||
public override string Acronym => "MR";
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override string Description => "Notes are flipped horizontally.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
// As the note is being held, adjust the size of the sizing container. This has two effects:
|
||||
// 1. The contained masking container will mask the body and ticks.
|
||||
// 2. The head note will move along with the new "head position" in the container.
|
||||
if (Head.IsHit && releaseTime == null)
|
||||
if (Head.IsHit && releaseTime == null && DrawHeight > 0)
|
||||
{
|
||||
// How far past the hit target this hold note is. Always a positive value.
|
||||
float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y);
|
||||
|
||||
@@ -70,10 +70,6 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
}
|
||||
}
|
||||
|
||||
// todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
|
||||
if (Replay.Frames.Count == 0)
|
||||
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time - 1));
|
||||
|
||||
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckOffscreenObjectsTest
|
||||
{
|
||||
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE * 0.5f;
|
||||
|
||||
private CheckOffscreenObjects check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckOffscreenObjects();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleInCenter()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleNearEdge()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(5, 5)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleNearEdgeStackedOffscreen()
|
||||
{
|
||||
assertOffscreenCircle(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(5, 5),
|
||||
StackHeight = 5
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleOffscreen()
|
||||
{
|
||||
assertOffscreenCircle(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(0, 0)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderInCenter()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(420, 240),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(-100, 0))
|
||||
}),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderNearEdge()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
|
||||
}),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderNearEdgeStackedOffscreen()
|
||||
{
|
||||
assertOffscreenSlider(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
|
||||
}),
|
||||
StackHeight = 5
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderOffscreenStart()
|
||||
{
|
||||
assertOffscreenSlider(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(playfield_centre)
|
||||
}),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderOffscreenEnd()
|
||||
{
|
||||
assertOffscreenSlider(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(-playfield_centre)
|
||||
}),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderOffscreenPath()
|
||||
{
|
||||
assertOffscreenSlider(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
// Circular arc shoots over the top of the screen.
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(-100, -200)),
|
||||
new PathControlPoint(new Vector2(100, -200))
|
||||
}),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void assertOk(IBeatmap beatmap)
|
||||
{
|
||||
Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertOffscreenCircle(IBeatmap beatmap)
|
||||
{
|
||||
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
|
||||
}
|
||||
|
||||
private void assertOffscreenSlider(IBeatmap beatmap)
|
||||
{
|
||||
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneOsuEditorSelectInvalidPath : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
[Test]
|
||||
public void TestSelectDoesNotModify()
|
||||
{
|
||||
Slider slider = new Slider { StartTime = 0, Position = new Vector2(320, 40) };
|
||||
|
||||
PathControlPoint[] points =
|
||||
{
|
||||
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(-100, 0)),
|
||||
new PathControlPoint(new Vector2(100, 20))
|
||||
};
|
||||
|
||||
int preSelectVersion = -1;
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
slider.Path = new SliderPath(points);
|
||||
EditorBeatmap.Add(slider);
|
||||
preSelectVersion = slider.Path.Version.Value;
|
||||
});
|
||||
|
||||
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddAssert("slider same path", () => slider.Path.Version.Value == preSelectVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
@@ -14,7 +16,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestScenePathControlPointVisualiser : OsuTestScene
|
||||
public class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene
|
||||
{
|
||||
private Slider slider;
|
||||
private PathControlPointVisualiser visualiser;
|
||||
@@ -43,12 +45,145 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerfectCurveTooManyPoints()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
addControlPointStep(new Vector2(700, 200));
|
||||
addControlPointStep(new Vector2(500, 100));
|
||||
|
||||
// Must be both hovering and selecting the control point for the context menu to work.
|
||||
moveMouseToControlPoint(1);
|
||||
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
|
||||
addContextMenuItemStep("Perfect curve");
|
||||
|
||||
assertControlPointPathType(0, PathType.Bezier);
|
||||
assertControlPointPathType(1, PathType.PerfectCurve);
|
||||
assertControlPointPathType(3, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerfectCurveLastThreePoints()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
addControlPointStep(new Vector2(700, 200));
|
||||
addControlPointStep(new Vector2(500, 100));
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true);
|
||||
addContextMenuItemStep("Perfect curve");
|
||||
|
||||
assertControlPointPathType(0, PathType.Bezier);
|
||||
assertControlPointPathType(2, PathType.PerfectCurve);
|
||||
assertControlPointPathType(4, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerfectCurveLastTwoPoints()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
addControlPointStep(new Vector2(700, 200));
|
||||
addControlPointStep(new Vector2(500, 100));
|
||||
|
||||
moveMouseToControlPoint(3);
|
||||
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
|
||||
addContextMenuItemStep("Perfect curve");
|
||||
|
||||
assertControlPointPathType(0, PathType.Bezier);
|
||||
AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerfectCurveTooManyPointsLinear()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200), PathType.Linear);
|
||||
addControlPointStep(new Vector2(300));
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
addControlPointStep(new Vector2(700, 200));
|
||||
addControlPointStep(new Vector2(500, 100));
|
||||
|
||||
// Must be both hovering and selecting the control point for the context menu to work.
|
||||
moveMouseToControlPoint(1);
|
||||
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
|
||||
addContextMenuItemStep("Perfect curve");
|
||||
|
||||
assertControlPointPathType(0, PathType.Linear);
|
||||
assertControlPointPathType(1, PathType.PerfectCurve);
|
||||
assertControlPointPathType(3, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerfectCurveChangeToBezier()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||
addControlPointStep(new Vector2(300), PathType.PerfectCurve);
|
||||
addControlPointStep(new Vector2(500, 300));
|
||||
addControlPointStep(new Vector2(700, 200), PathType.Bezier);
|
||||
addControlPointStep(new Vector2(500, 100));
|
||||
|
||||
moveMouseToControlPoint(3);
|
||||
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
|
||||
addContextMenuItemStep("Inherit");
|
||||
|
||||
assertControlPointPathType(0, PathType.Bezier);
|
||||
assertControlPointPathType(1, PathType.Bezier);
|
||||
assertControlPointPathType(3, null);
|
||||
}
|
||||
|
||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
|
||||
private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position)));
|
||||
private void addControlPointStep(Vector2 position) => addControlPointStep(position, null);
|
||||
|
||||
private void addControlPointStep(Vector2 position, PathType? type)
|
||||
{
|
||||
AddStep($"add {type} control point at {position}", () =>
|
||||
{
|
||||
slider.Path.ControlPoints.Add(new PathControlPoint(position, type));
|
||||
});
|
||||
}
|
||||
|
||||
private void moveMouseToControlPoint(int index)
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
Vector2 position = slider.Path.ControlPoints[index].Position.Value;
|
||||
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertControlPointPathType(int controlPointIndex, PathType? type)
|
||||
{
|
||||
AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type);
|
||||
}
|
||||
|
||||
private void addContextMenuItemStep(string contextMenuText)
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
item?.Action?.Value();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public class TestSceneSliderControlPointPiece : SelectionBlueprintTestScene
|
||||
{
|
||||
private Slider slider;
|
||||
private DrawableSlider drawableObject;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Clear();
|
||||
|
||||
slider = new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(150, 150)),
|
||||
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(400, 0)),
|
||||
new PathControlPoint(new Vector2(400, 150))
|
||||
})
|
||||
};
|
||||
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
|
||||
|
||||
Add(drawableObject = new DrawableSlider(slider));
|
||||
AddBlueprint(new TestSliderBlueprint(drawableObject));
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestDragControlPoint()
|
||||
{
|
||||
moveMouseToControlPoint(1);
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
addMovementStep(new Vector2(150, 50));
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
assertControlPointPosition(1, new Vector2(150, 50));
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragControlPointAlmostLinearlyExterior()
|
||||
{
|
||||
moveMouseToControlPoint(1);
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
addMovementStep(new Vector2(400, 0.01f));
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
assertControlPointPosition(1, new Vector2(400, 0.01f));
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragControlPointPathRecovery()
|
||||
{
|
||||
moveMouseToControlPoint(1);
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
addMovementStep(new Vector2(400, 0.01f));
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
|
||||
addMovementStep(new Vector2(150, 50));
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
assertControlPointPosition(1, new Vector2(150, 50));
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragControlPointPathRecoveryOtherSegment()
|
||||
{
|
||||
moveMouseToControlPoint(4);
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
addMovementStep(new Vector2(350, 0.01f));
|
||||
assertControlPointType(2, PathType.Bezier);
|
||||
|
||||
addMovementStep(new Vector2(150, 150));
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
assertControlPointPosition(4, new Vector2(150, 150));
|
||||
assertControlPointType(2, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDragControlPointPathAfterChangingType()
|
||||
{
|
||||
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier);
|
||||
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
|
||||
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve);
|
||||
|
||||
moveMouseToControlPoint(4);
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
assertControlPointType(3, PathType.PerfectCurve);
|
||||
|
||||
addMovementStep(new Vector2(350, 0.01f));
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
assertControlPointPosition(4, new Vector2(350, 0.01f));
|
||||
assertControlPointType(3, PathType.Bezier);
|
||||
}
|
||||
|
||||
private void addMovementStep(Vector2 relativePosition)
|
||||
{
|
||||
AddStep($"move mouse to {relativePosition}", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + relativePosition;
|
||||
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
private void moveMouseToControlPoint(int index)
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
|
||||
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
|
||||
});
|
||||
}
|
||||
|
||||
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type);
|
||||
|
||||
private void assertControlPointPosition(int index, Vector2 position) =>
|
||||
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1));
|
||||
|
||||
private class TestSliderBlueprint : SliderSelectionBlueprint
|
||||
{
|
||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
|
||||
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
|
||||
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
|
||||
public TestSliderBlueprint(DrawableSlider slider)
|
||||
: base(slider)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position);
|
||||
}
|
||||
|
||||
private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint
|
||||
{
|
||||
public new HitCirclePiece CirclePiece => base.CirclePiece;
|
||||
|
||||
public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position)
|
||||
: base(slider, position)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneSliderLengthValidity : TestSceneOsuEditor
|
||||
{
|
||||
private OsuPlayfield playfield;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
|
||||
AddStep("seek to first timing point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDraggingStartingPointRemainsValid()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("Add slider", () =>
|
||||
{
|
||||
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
|
||||
|
||||
PathControlPoint[] points =
|
||||
{
|
||||
new PathControlPoint(new Vector2(0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
};
|
||||
|
||||
slider.Path = new SliderPath(points);
|
||||
EditorBeatmap.Add(slider);
|
||||
});
|
||||
|
||||
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
|
||||
moveMouse(new Vector2(300));
|
||||
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
double distanceBefore = 0;
|
||||
|
||||
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
|
||||
|
||||
moveMouse(new Vector2(300, 300));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
moveMouse(new Vector2(350, 300));
|
||||
moveMouse(new Vector2(400, 300));
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
|
||||
AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDraggingEndingPointRemainsValid()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("Add slider", () =>
|
||||
{
|
||||
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
|
||||
|
||||
PathControlPoint[] points =
|
||||
{
|
||||
new PathControlPoint(new Vector2(0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
};
|
||||
|
||||
slider.Path = new SliderPath(points);
|
||||
EditorBeatmap.Add(slider);
|
||||
});
|
||||
|
||||
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
|
||||
moveMouse(new Vector2(300));
|
||||
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
double distanceBefore = 0;
|
||||
|
||||
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
|
||||
|
||||
moveMouse(new Vector2(400, 300));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
moveMouse(new Vector2(350, 300));
|
||||
moveMouse(new Vector2(300, 300));
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
|
||||
AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a control point is deleted which results in the slider becoming so short it can't exist,
|
||||
/// for simplicity delete the slider rather than having it in an invalid state.
|
||||
///
|
||||
/// Eventually we may need to change this, based on user feedback. I think it's likely enough of
|
||||
/// an edge case that we won't get many complaints, though (and there's always the undo button).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestDeletingPointCausesSliderDeletion()
|
||||
{
|
||||
AddStep("Add slider", () =>
|
||||
{
|
||||
Slider slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
|
||||
|
||||
PathControlPoint[] points =
|
||||
{
|
||||
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(0, 10))
|
||||
};
|
||||
|
||||
slider.Path = new SliderPath(points);
|
||||
EditorBeatmap.Add(slider);
|
||||
});
|
||||
|
||||
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
|
||||
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
moveMouse(new Vector2(400, 300));
|
||||
AddStep("delete second point", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
AddAssert("ensure object deleted", () => EditorBeatmap.HitObjects.Count == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a scale operation is performed where a single slider is the only thing selected, the path's shape will change.
|
||||
/// If the scale results in the path becoming too short, further mouse movement in the same direction will not change the shape.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestScalingSliderTooSmallRemainsValid()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("Add slider", () =>
|
||||
{
|
||||
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300, 200) };
|
||||
|
||||
PathControlPoint[] points =
|
||||
{
|
||||
new PathControlPoint(new Vector2(0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(0, 50)),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
};
|
||||
|
||||
slider.Path = new SliderPath(points);
|
||||
EditorBeatmap.Add(slider);
|
||||
});
|
||||
|
||||
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
|
||||
moveMouse(new Vector2(300));
|
||||
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
double distanceBefore = 0;
|
||||
|
||||
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<SelectionBoxDragHandle>().Skip(1).First()));
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
moveMouse(new Vector2(300, 300));
|
||||
moveMouse(new Vector2(300, 250));
|
||||
moveMouse(new Vector2(300, 200));
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
|
||||
AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
|
||||
}
|
||||
|
||||
private void moveMouse(Vector2 pos) =>
|
||||
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
addClickStep(MouseButton.Left);
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(0);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
assertPlaced(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -276,6 +274,104 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
|
||||
{
|
||||
Vector2 startPosition = new Vector2(200);
|
||||
|
||||
addMovementStep(startPosition);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(startPosition + new Vector2(300, 0));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(startPosition + new Vector2(150, 0.1f));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacePerfectCurveSegmentRecovery()
|
||||
{
|
||||
Vector2 startPosition = new Vector2(200);
|
||||
|
||||
addMovementStep(startPosition);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(startPosition + new Vector2(300, 0));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(startPosition + new Vector2(150, 0.1f)); // Should convert to bezier
|
||||
addMovementStep(startPosition + new Vector2(400.0f, 50.0f)); // Should convert back to perfect
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacePerfectCurveSegmentLarge()
|
||||
{
|
||||
Vector2 startPosition = new Vector2(400);
|
||||
|
||||
addMovementStep(startPosition);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(startPosition + new Vector2(220, 220));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
// Playfield dimensions are 640 x 480.
|
||||
// So a 440 x 440 bounding box should be ok.
|
||||
addMovementStep(startPosition + new Vector2(-220, 220));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacePerfectCurveSegmentTooLarge()
|
||||
{
|
||||
Vector2 startPosition = new Vector2(480, 200);
|
||||
|
||||
addMovementStep(startPosition);
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(startPosition + new Vector2(400, 400));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
// Playfield dimensions are 640 x 480.
|
||||
// So an 800 * 800 bounding box area should not be ok.
|
||||
addMovementStep(startPosition + new Vector2(-400, 400));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.Bezier);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacePerfectCurveSegmentCompleteArc()
|
||||
{
|
||||
addMovementStep(new Vector2(400));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(600, 400));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
addMovementStep(new Vector2(400, 410));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(3);
|
||||
assertControlPointType(0, PathType.PerfectCurve);
|
||||
}
|
||||
|
||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||
|
||||
private void addClickStep(MouseButton button)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSliderScaling : TestSceneOsuEditor
|
||||
{
|
||||
private OsuPlayfield playfield;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
|
||||
AddStep("seek to first timing point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScalingLinearSlider()
|
||||
{
|
||||
Slider slider = null;
|
||||
|
||||
AddStep("Add slider", () =>
|
||||
{
|
||||
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
|
||||
|
||||
PathControlPoint[] points =
|
||||
{
|
||||
new PathControlPoint(new Vector2(0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
};
|
||||
|
||||
slider.Path = new SliderPath(points);
|
||||
EditorBeatmap.Add(slider);
|
||||
});
|
||||
|
||||
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
|
||||
moveMouse(new Vector2(300));
|
||||
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
double distanceBefore = 0;
|
||||
|
||||
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<SelectionBoxDragHandle>().Skip(1).First()));
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
moveMouse(new Vector2(300, 300));
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
|
||||
}
|
||||
|
||||
private void moveMouse(Vector2 pos) =>
|
||||
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
private void runSpmTest(Mod mod)
|
||||
{
|
||||
SpinnerSpmCounter spmCounter = null;
|
||||
SpinnerSpmCalculator spmCalculator = null;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
@@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
|
||||
});
|
||||
|
||||
AddUntilStep("fetch SPM counter", () =>
|
||||
AddUntilStep("fetch SPM calculator", () =>
|
||||
{
|
||||
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
||||
return spmCounter != null;
|
||||
spmCalculator = this.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||
return spmCalculator != null;
|
||||
});
|
||||
|
||||
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
|
||||
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
Beatmap = singleSpinnerBeatmap,
|
||||
PassCondition = () =>
|
||||
{
|
||||
var counter = Player.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
||||
return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
|
||||
var counter = Player.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||
return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
get
|
||||
{
|
||||
if (content == null)
|
||||
base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
|
||||
base.Content.Add(content = new OsuInputManager(new OsuRuleset().RulesetInfo));
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => null;
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
{
|
||||
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
return null;
|
||||
}
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
|
||||
public event Action SourceChanged
|
||||
{
|
||||
|
||||
@@ -4,13 +4,22 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@@ -21,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Cached]
|
||||
private GameplayBeatmap gameplayBeatmap;
|
||||
|
||||
private ClickingCursorContainer lastContainer;
|
||||
private OsuCursorContainer lastContainer;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
@@ -48,12 +57,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
config.SetValue(OsuSetting.AutoCursorSize, true);
|
||||
gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
|
||||
Scheduler.AddOnce(recreate);
|
||||
Scheduler.AddOnce(() => loadContent(false));
|
||||
});
|
||||
|
||||
AddStep("test cursor container", recreate);
|
||||
|
||||
void recreate() => SetContents(() => new OsuInputManager(new OsuRuleset().RulesetInfo) { Child = new OsuCursorContainer() });
|
||||
AddStep("test cursor container", () => loadContent(false));
|
||||
}
|
||||
|
||||
[TestCase(1, 1)]
|
||||
@@ -68,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
|
||||
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
|
||||
|
||||
AddStep("load content", loadContent);
|
||||
AddStep("load content", () => loadContent());
|
||||
|
||||
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
|
||||
|
||||
@@ -82,18 +89,46 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
|
||||
}
|
||||
|
||||
private void loadContent()
|
||||
[Test]
|
||||
public void TestTopLeftOrigin()
|
||||
{
|
||||
SetContents(() => new MovingCursorInputManager
|
||||
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
|
||||
}
|
||||
|
||||
private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null)
|
||||
{
|
||||
SetContents(() =>
|
||||
{
|
||||
Child = lastContainer = new ClickingCursorContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
}
|
||||
var inputManager = automated ? (InputManager)new MovingCursorInputManager() : new OsuInputManager(new OsuRuleset().RulesetInfo);
|
||||
var skinContainer = skinProvider?.Invoke() ?? new SkinProvidingContainer(null);
|
||||
|
||||
lastContainer = automated ? new ClickingCursorContainer() : new OsuCursorContainer();
|
||||
|
||||
return inputManager.WithChild(skinContainer.WithChild(lastContainer));
|
||||
});
|
||||
}
|
||||
|
||||
private class TopLeftCursorSkin : ISkin
|
||||
{
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => null;
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case OsuSkinConfiguration osuLookup:
|
||||
if (osuLookup == OsuSkinConfiguration.CursorCentre)
|
||||
return SkinUtils.As<TValue>(new BindableBool(false));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class ClickingCursorContainer : OsuCursorContainer
|
||||
{
|
||||
private bool pressed;
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
Position = new Vector2(128, 128),
|
||||
ComboIndex = 1,
|
||||
}), null));
|
||||
})));
|
||||
}
|
||||
|
||||
private HitCircle prepareObject(HitCircle circle)
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new Vector2(300, 0),
|
||||
}),
|
||||
RepeatCount = 1
|
||||
}), null));
|
||||
})));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Position = new Vector2(256, 192),
|
||||
ComboIndex = 1,
|
||||
Duration = 1000,
|
||||
}), null));
|
||||
})));
|
||||
|
||||
AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
@@ -168,13 +169,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
double estimatedSpm = 0;
|
||||
|
||||
addSeekStep(1000);
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
|
||||
|
||||
addSeekStep(2000);
|
||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||
|
||||
addSeekStep(1000);
|
||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||
}
|
||||
|
||||
[TestCase(0.5)]
|
||||
@@ -188,16 +189,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("retrieve spinner state", () =>
|
||||
{
|
||||
expectedProgress = drawableSpinner.Progress;
|
||||
expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
|
||||
expectedSpm = drawableSpinner.SpinsPerMinute.Value;
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
|
||||
AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate);
|
||||
AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
|
||||
|
||||
addSeekStep(1000);
|
||||
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
|
||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
|
||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
||||
}
|
||||
|
||||
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -28,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
|
||||
{
|
||||
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
|
||||
public List<PathControlPoint> PointsInSegment;
|
||||
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
@@ -54,6 +59,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
this.slider = slider;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the slider.
|
||||
cachePoints(slider);
|
||||
|
||||
slider.Path.Version.BindValueChanged(_ =>
|
||||
{
|
||||
cachePoints(slider);
|
||||
updatePathType();
|
||||
});
|
||||
|
||||
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
@@ -150,6 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
|
||||
|
||||
private Vector2 dragStartPosition;
|
||||
private PathType? dragPathType;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
@@ -159,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (e.Button == MouseButton.Left)
|
||||
{
|
||||
dragStartPosition = ControlPoint.Position.Value;
|
||||
dragPathType = PointsInSegment[0].Type.Value;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
return true;
|
||||
}
|
||||
@@ -168,6 +185,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position.Value).ToArray();
|
||||
var oldPosition = slider.Position;
|
||||
var oldStartTime = slider.StartTime;
|
||||
|
||||
if (ControlPoint == slider.Path.ControlPoints[0])
|
||||
{
|
||||
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
|
||||
@@ -184,10 +205,45 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
else
|
||||
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
|
||||
|
||||
if (!slider.Path.HasValidLength)
|
||||
{
|
||||
for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position.Value = oldControlPoints[i];
|
||||
|
||||
slider.Position = oldPosition;
|
||||
slider.StartTime = oldStartTime;
|
||||
return;
|
||||
}
|
||||
|
||||
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
|
||||
PointsInSegment[0].Type.Value = dragPathType;
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
|
||||
|
||||
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
/// </summary>
|
||||
private void updatePathType()
|
||||
{
|
||||
if (ControlPoint.Type.Value != PathType.PerfectCurve)
|
||||
return;
|
||||
|
||||
if (PointsInSegment.Count > 3)
|
||||
ControlPoint.Type.Value = PathType.Bezier;
|
||||
|
||||
if (PointsInSegment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position.Value).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
ControlPoint.Type.Value = PathType.Bezier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the circular control point marker.
|
||||
/// </summary>
|
||||
|
||||
+29
-1
@@ -153,6 +153,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set the given control point piece to the given path type.
|
||||
/// If that would fail, try to change the path such that it instead succeeds
|
||||
/// in a UX-friendly way.
|
||||
/// </summary>
|
||||
/// <param name="piece">The control point piece that we want to change the path type of.</param>
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathType(PathControlPointPiece piece, PathType? type)
|
||||
{
|
||||
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case PathType.PerfectCurve:
|
||||
// Can't always create a circular arc out of 4 or more points,
|
||||
// so we split the segment into one 3-point circular arc segment
|
||||
// and one segment of the previous type.
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
|
||||
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
|
||||
piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
piece.ControlPoint.Type.Value = type;
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
@@ -218,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
var item = new PathTypeMenuItem(type, () =>
|
||||
{
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
p.ControlPoint.Type.Value = type;
|
||||
updatePathType(p, type);
|
||||
});
|
||||
|
||||
if (countOfState == totalCount)
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
private PlacementState state;
|
||||
private SliderPlacementState state;
|
||||
private PathControlPoint segmentStart;
|
||||
private PathControlPoint cursor;
|
||||
private int currentSegmentLength;
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
|
||||
};
|
||||
|
||||
setState(PlacementState.Initial);
|
||||
setState(SliderPlacementState.Initial);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -73,12 +73,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case PlacementState.Initial:
|
||||
case SliderPlacementState.Initial:
|
||||
BeginPlacement();
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
break;
|
||||
|
||||
case PlacementState.Body:
|
||||
case SliderPlacementState.Body:
|
||||
updateCursor();
|
||||
break;
|
||||
}
|
||||
@@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case PlacementState.Initial:
|
||||
case SliderPlacementState.Initial:
|
||||
beginCurve();
|
||||
break;
|
||||
|
||||
case PlacementState.Body:
|
||||
case SliderPlacementState.Body:
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
{
|
||||
// Place a new point by detatching the current cursor.
|
||||
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
if (state == PlacementState.Body && e.Button == MouseButton.Right)
|
||||
if (state == SliderPlacementState.Body && e.Button == MouseButton.Right)
|
||||
endCurve();
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
@@ -129,19 +129,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private void beginCurve()
|
||||
{
|
||||
BeginPlacement(commitStart: true);
|
||||
setState(PlacementState.Body);
|
||||
setState(SliderPlacementState.Body);
|
||||
}
|
||||
|
||||
private void endCurve()
|
||||
{
|
||||
updateSlider();
|
||||
EndPlacement(true);
|
||||
EndPlacement(HitObject.Path.HasValidLength);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updateSlider();
|
||||
|
||||
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
|
||||
updatePathType();
|
||||
}
|
||||
|
||||
private void updatePathType()
|
||||
@@ -216,12 +219,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
tailCirclePiece.UpdateFrom(HitObject.TailCircle);
|
||||
}
|
||||
|
||||
private void setState(PlacementState newState)
|
||||
private void setState(SliderPlacementState newState)
|
||||
{
|
||||
state = newState;
|
||||
}
|
||||
|
||||
private enum PlacementState
|
||||
private enum SliderPlacementState
|
||||
{
|
||||
Initial,
|
||||
Body,
|
||||
|
||||
@@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
|
||||
if (controlPoints.Count <= 1)
|
||||
if (controlPoints.Count <= 1 || !slider.HitObject.Path.HasValidLength)
|
||||
{
|
||||
placementHandler?.Delete(HitObject);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// 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.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckOffscreenObjects : ICheck
|
||||
{
|
||||
// A close approximation for the bounding box of the screen in gameplay on 4:3 aspect ratio.
|
||||
// Uses gameplay space coordinates (512 x 384 playfield / 640 x 480 screen area).
|
||||
// See https://github.com/ppy/osu/pull/12361#discussion_r612199777 for reference.
|
||||
private const int min_x = -67;
|
||||
private const int min_y = -60;
|
||||
private const int max_x = 579;
|
||||
private const int max_y = 428;
|
||||
|
||||
// The amount of milliseconds to step through a slider path at a time
|
||||
// (higher = more performant, but higher false-negative chance).
|
||||
private const int path_step_size = 5;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Offscreen hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateOffscreenCircle(this),
|
||||
new IssueTemplateOffscreenSlider(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
{
|
||||
foreach (var hitobject in playableBeatmap.HitObjects)
|
||||
{
|
||||
switch (hitobject)
|
||||
{
|
||||
case Slider slider:
|
||||
{
|
||||
foreach (var issue in sliderIssues(slider))
|
||||
yield return issue;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case HitCircle circle:
|
||||
{
|
||||
if (isOffscreen(circle.StackedPosition, circle.Radius))
|
||||
yield return new IssueTemplateOffscreenCircle(this).Create(circle);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Steps through points on the slider to ensure the entire path is on-screen.
|
||||
/// Returns at most one issue.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider whose path to check.</param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<Issue> sliderIssues(Slider slider)
|
||||
{
|
||||
for (int i = 0; i < slider.Distance; i += path_step_size)
|
||||
{
|
||||
double progress = i / slider.Distance;
|
||||
Vector2 position = slider.StackedPositionAt(progress);
|
||||
|
||||
if (!isOffscreen(position, slider.Radius))
|
||||
continue;
|
||||
|
||||
// `SpanDuration` ensures we don't include reverses.
|
||||
double time = slider.StartTime + progress * slider.SpanDuration;
|
||||
yield return new IssueTemplateOffscreenSlider(this).Create(slider, time);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Above loop may skip the last position in the slider due to step size.
|
||||
if (!isOffscreen(slider.StackedEndPosition, slider.Radius))
|
||||
yield break;
|
||||
|
||||
yield return new IssueTemplateOffscreenSlider(this).Create(slider, slider.EndTime);
|
||||
}
|
||||
|
||||
private bool isOffscreen(Vector2 position, double radius)
|
||||
{
|
||||
return position.X - radius < min_x || position.X + radius > max_x ||
|
||||
position.Y - radius < min_y || position.Y + radius > max_y;
|
||||
}
|
||||
|
||||
public class IssueTemplateOffscreenCircle : IssueTemplate
|
||||
{
|
||||
public IssueTemplateOffscreenCircle(ICheck check)
|
||||
: base(check, IssueType.Problem, "This circle goes offscreen on a 4:3 aspect ratio.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitCircle circle) => new Issue(circle, this);
|
||||
}
|
||||
|
||||
public class IssueTemplateOffscreenSlider : IssueTemplate
|
||||
{
|
||||
public IssueTemplateOffscreenSlider(ICheck check)
|
||||
: base(check, IssueType.Problem, "This slider goes offscreen here on a 4:3 aspect ratio.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(Slider slider, double offscreenTime) => new Issue(slider, this) { Time = offscreenTime };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
@@ -27,8 +30,16 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private class OsuEditPlayfield : OsuPlayfield
|
||||
{
|
||||
private Bindable<bool> hitAnimations;
|
||||
|
||||
protected override GameplayCursorContainer CreateCursor() => null;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
hitAnimations = config.GetBindable<bool>(OsuSetting.EditorHitAnimations);
|
||||
}
|
||||
|
||||
protected override void OnNewDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
d.ApplyCustomUpdateState += updateState;
|
||||
@@ -42,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private void updateState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
if (state == ArmedState.Idle)
|
||||
if (state == ArmedState.Idle || hitAnimations.Value)
|
||||
return;
|
||||
|
||||
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
|
||||
@@ -58,8 +69,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
case DrawableHitCircle circle: // also handles slider heads
|
||||
circle.ApproachCircle
|
||||
.FadeOutFromOne(editor_hit_object_fade_out_extension)
|
||||
.FadeOutFromOne(editor_hit_object_fade_out_extension * 4)
|
||||
.Expire();
|
||||
|
||||
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
|
||||
|
||||
var circlePieceDrawable = circle.CirclePiece.Drawable;
|
||||
|
||||
// clear any explode animation logic.
|
||||
circlePieceDrawable.ApplyTransformsAt(circle.HitStateUpdateTime, true);
|
||||
circlePieceDrawable.ClearTransformsAfter(circle.HitStateUpdateTime, true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -71,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
hitObject.RemoveTransform(existing);
|
||||
|
||||
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
|
||||
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
|
||||
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuBeatmapVerifier : IBeatmapVerifier
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
new CheckOffscreenObjects()
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
|
||||
{
|
||||
return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
referenceOrigin = null;
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||
@@ -53,6 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// </summary>
|
||||
private Vector2? referenceOrigin;
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||
/// can be maintained throughout the operation.
|
||||
/// </summary>
|
||||
private List<PathType?> referencePathTypes;
|
||||
|
||||
public override bool HandleReverse()
|
||||
{
|
||||
var hitObjects = EditorBeatmap.SelectedHitObjects;
|
||||
@@ -194,12 +201,16 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale)
|
||||
{
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
|
||||
|
||||
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
|
||||
|
||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
||||
|
||||
Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height);
|
||||
Vector2 pathRelativeDeltaScale = new Vector2(
|
||||
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
|
||||
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
|
||||
|
||||
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
|
||||
|
||||
@@ -209,11 +220,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
point.Position.Value *= pathRelativeDeltaScale;
|
||||
}
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
|
||||
slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i];
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds)
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
return;
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToDrawableHitObjects
|
||||
{
|
||||
private float currentRotation;
|
||||
|
||||
[SettingSource("Roll speed", "Rotations per minute")]
|
||||
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
|
||||
{
|
||||
MinValue = 0.02,
|
||||
MaxValue = 12,
|
||||
Precision = 0.01,
|
||||
};
|
||||
|
||||
[SettingSource("Direction", "The direction of rotation")]
|
||||
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
|
||||
|
||||
public override string Name => "Barrel Roll";
|
||||
public override string Acronym => "BR";
|
||||
public override string Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
playfield.Rotation = currentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// scale the playfield to allow all hitobjects to stay within the visible region.
|
||||
drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
{
|
||||
foreach (var d in drawables)
|
||||
{
|
||||
d.OnUpdate += _ =>
|
||||
{
|
||||
switch (d)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
circle.CirclePiece.Rotation = -currentRotation;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -16,22 +15,8 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
public override string Name => "Classic";
|
||||
|
||||
public override string Acronym => "CL";
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override IconUsage? Icon => FontAwesome.Solid.History;
|
||||
|
||||
public override string Description => "Feeling nostalgic?";
|
||||
|
||||
public override bool Ranked => false;
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@@ -23,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private const float default_flashlight_size = 180;
|
||||
|
||||
private const double default_follow_delay = 120;
|
||||
|
||||
private OsuFlashlight flashlight;
|
||||
|
||||
public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight();
|
||||
@@ -35,8 +39,25 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public override void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
base.ApplyToDrawableRuleset(drawableRuleset);
|
||||
|
||||
flashlight.FollowDelay = FollowDelay.Value;
|
||||
}
|
||||
|
||||
[SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")]
|
||||
public BindableNumber<double> FollowDelay { get; } = new BindableDouble(default_follow_delay)
|
||||
{
|
||||
MinValue = default_follow_delay,
|
||||
MaxValue = default_follow_delay * 10,
|
||||
Precision = default_follow_delay,
|
||||
};
|
||||
|
||||
private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
|
||||
{
|
||||
public double FollowDelay { private get; set; }
|
||||
|
||||
public OsuFlashlight()
|
||||
{
|
||||
FlashlightSize = new Vector2(0, getSizeFor(0));
|
||||
@@ -50,13 +71,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
const double follow_delay = 120;
|
||||
|
||||
var position = FlashlightPosition;
|
||||
var destination = e.MousePosition;
|
||||
|
||||
FlashlightPosition = Interpolation.ValueAt(
|
||||
Math.Min(Math.Abs(Clock.ElapsedFrameTime), follow_delay), position, destination, 0, follow_delay, Easing.Out);
|
||||
Math.Min(Math.Abs(Clock.ElapsedFrameTime), FollowDelay), position, destination, 0, FollowDelay, Easing.Out);
|
||||
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Name => "Touch Device";
|
||||
public override string Acronym => "TD";
|
||||
public override string Description => "Automatically applied to plays on devices with a touchscreen.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override ModType Type => ModType.System;
|
||||
|
||||
@@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
var osuObject = (OsuHitObject)drawable.HitObject;
|
||||
Vector2 origin = drawable.Position;
|
||||
|
||||
// Wiggle the repeat points with the slider instead of independently.
|
||||
// Wiggle the repeat points and the tail with the slider instead of independently.
|
||||
// Also fixes an issue with repeat points being positioned incorrectly.
|
||||
if (osuObject is SliderRepeat)
|
||||
if (osuObject is SliderRepeat || osuObject is SliderTailCircle)
|
||||
return;
|
||||
|
||||
Random objRand = new Random((int)osuObject.StartTime);
|
||||
|
||||
@@ -66,7 +66,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return true;
|
||||
},
|
||||
},
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece()),
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
ApproachCircle = new ApproachCircle
|
||||
{
|
||||
Alpha = 0,
|
||||
@@ -178,6 +182,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// todo: temporary / arbitrary, used for lifetime optimisation.
|
||||
this.Delay(800).FadeOut();
|
||||
|
||||
(CirclePiece.Drawable as IMainCirclePiece)?.Animate(state);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osuTK;
|
||||
@@ -108,16 +109,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected override void LoadSamples()
|
||||
{
|
||||
base.LoadSamples();
|
||||
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
|
||||
|
||||
var firstSample = HitObject.Samples.FirstOrDefault();
|
||||
|
||||
if (firstSample != null)
|
||||
if (HitObject.SampleControlPoint == null)
|
||||
{
|
||||
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide");
|
||||
|
||||
slidingSample.Samples = new ISampleInfo[] { clone };
|
||||
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
|
||||
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||
}
|
||||
|
||||
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
|
||||
|
||||
var slidingSamples = new List<ISampleInfo>();
|
||||
|
||||
var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
|
||||
if (normalSample != null)
|
||||
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide"));
|
||||
|
||||
var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
|
||||
if (whistleSample != null)
|
||||
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle"));
|
||||
|
||||
slidingSample.Samples = slidingSamples.ToArray();
|
||||
}
|
||||
|
||||
public override void StopAllSamples()
|
||||
|
||||
@@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||
|
||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||
public SpinnerSpmCounter SpmCounter { get; private set; }
|
||||
|
||||
private SpinnerSpmCalculator spmCalculator;
|
||||
|
||||
private Container<DrawableSpinnerTick> ticks;
|
||||
private PausableSkinnableSound spinningSample;
|
||||
@@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public IBindable<double> GainedBonus => gainedBonus;
|
||||
|
||||
private readonly Bindable<double> gainedBonus = new Bindable<double>();
|
||||
private readonly Bindable<double> gainedBonus = new BindableDouble();
|
||||
|
||||
/// <summary>
|
||||
/// The number of spins per minute this spinner is spinning at, for display purposes.
|
||||
/// </summary>
|
||||
public readonly IBindable<double> SpinsPerMinute = new BindableDouble();
|
||||
|
||||
private const double fade_out_duration = 160;
|
||||
|
||||
@@ -63,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
spmCalculator = new SpinnerSpmCalculator
|
||||
{
|
||||
Result = { BindTarget = SpinsPerMinute },
|
||||
},
|
||||
ticks = new Container<DrawableSpinnerTick>(),
|
||||
new AspectContainer
|
||||
{
|
||||
@@ -77,20 +87,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
RotationTracker = new SpinnerRotationTracker(this)
|
||||
}
|
||||
},
|
||||
SpmCounter = new SpinnerSpmCounter
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = 120,
|
||||
Alpha = 0
|
||||
},
|
||||
spinningSample = new PausableSkinnableSound
|
||||
{
|
||||
Volume = { Value = 0 },
|
||||
Looping = true,
|
||||
Frequency = { Value = spinning_sample_initial_frequency }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
|
||||
}
|
||||
@@ -161,17 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms()
|
||||
{
|
||||
base.UpdateStartTimeStateTransforms();
|
||||
|
||||
if (Result?.TimeStarted is double startTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(startTime))
|
||||
fadeInCounter();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
@@ -282,22 +274,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
|
||||
{
|
||||
Result.TimeStarted ??= Time.Current;
|
||||
fadeInCounter();
|
||||
}
|
||||
if (Result.TimeStarted == null && RotationTracker.Tracking)
|
||||
Result.TimeStarted = Time.Current;
|
||||
|
||||
// don't update after end time to avoid the rate display dropping during fade out.
|
||||
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
||||
if (Time.Current <= HitObject.EndTime)
|
||||
SpmCounter.SetRotation(Result.RateAdjustedRotation);
|
||||
spmCalculator.SetRotation(Result.RateAdjustedRotation);
|
||||
|
||||
updateBonusScore();
|
||||
}
|
||||
|
||||
private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
||||
|
||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||
|
||||
private int wholeSpins;
|
||||
|
||||
@@ -81,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
||||
|
||||
[JsonIgnore]
|
||||
public IList<HitSampleInfo> TailSamples { get; private set; }
|
||||
|
||||
private int repeatCount;
|
||||
|
||||
public int RepeatCount
|
||||
@@ -143,11 +146,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
|
||||
|
||||
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
|
||||
// For now, the samples are attached to and played by the slider itself at the correct end time.
|
||||
// ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live).
|
||||
Samples = this.GetNodeSamples(repeatCount + 1).ToArray();
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
@@ -238,6 +236,10 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
if (HeadCircle != null)
|
||||
HeadCircle.Samples = this.GetNodeSamples(0);
|
||||
|
||||
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
|
||||
// For now, the samples are played by the slider itself at the correct end time.
|
||||
TailSamples = this.GetNodeSamples(repeatCount + 1);
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
|
||||
|
||||
@@ -185,6 +185,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new MultiMod(new OsuModGrow(), new OsuModDeflate()),
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new OsuModTraceable(),
|
||||
new OsuModBarrelRoll(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
@@ -206,6 +207,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new OsuBeatmapVerifier();
|
||||
|
||||
public override string Description => "osu!";
|
||||
|
||||
public override string ShortName => SHORT_NAME;
|
||||
|
||||
@@ -71,8 +71,6 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
|
||||
buttonIndex = 0;
|
||||
|
||||
AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500)));
|
||||
AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
|
||||
AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
|
||||
|
||||
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Replays
|
||||
{
|
||||
@@ -21,24 +19,11 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
|
||||
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any();
|
||||
|
||||
protected Vector2? Position
|
||||
{
|
||||
get
|
||||
{
|
||||
var frame = CurrentFrame;
|
||||
|
||||
if (frame == null)
|
||||
return null;
|
||||
|
||||
Debug.Assert(CurrentTime != null);
|
||||
|
||||
return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CollectPendingInputs(List<IInput> inputs)
|
||||
{
|
||||
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
|
||||
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
|
||||
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(position) });
|
||||
inputs.Add(new ReplayState<OsuAction> { PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
private OsuSpriteText bonusCounter;
|
||||
|
||||
private Container spmContainer;
|
||||
private OsuSpriteText spmCounter;
|
||||
|
||||
public DefaultSpinner()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Numeric.With(size: 24),
|
||||
Y = -120,
|
||||
},
|
||||
spmContainer = new Container
|
||||
{
|
||||
Alpha = 0f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = 120,
|
||||
Children = new[]
|
||||
{
|
||||
spmCounter = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = @"0",
|
||||
Font = OsuFont.Numeric.With(size: 24)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = @"SPINS PER MINUTE",
|
||||
Font = OsuFont.Numeric.With(size: 12),
|
||||
Y = 30
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private IBindable<double> gainedBonus;
|
||||
private IBindable<double> spinsPerMinute;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
bonusCounter.FadeOutFromOne(1500);
|
||||
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||
});
|
||||
|
||||
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||
spinsPerMinute.BindValueChanged(spm =>
|
||||
{
|
||||
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||
}, true);
|
||||
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
|
||||
fadeCounterOnTimeStart();
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
fadeCounterOnTimeStart();
|
||||
}
|
||||
|
||||
private void fadeCounterOnTimeStart()
|
||||
{
|
||||
if (drawableSpinner.Result?.TimeStarted is double startTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(startTime))
|
||||
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +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 osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public interface IMainCirclePiece
|
||||
{
|
||||
/// <summary>
|
||||
/// Begins animating this <see cref="IMainCirclePiece"/>.
|
||||
/// </summary>
|
||||
/// <param name="state">The <see cref="ArmedState"/> of the related <see cref="DrawableHitCircle"/>.</param>
|
||||
void Animate(ArmedState state);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public class MainCirclePiece : CompositeDrawable
|
||||
public class MainCirclePiece : CompositeDrawable, IMainCirclePiece
|
||||
{
|
||||
private readonly CirclePiece circle;
|
||||
private readonly RingPiece ring;
|
||||
@@ -67,12 +67,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
}, true);
|
||||
|
||||
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
|
||||
|
||||
drawableObject.ApplyCustomUpdateState += updateState;
|
||||
updateState(drawableObject, drawableObject.State.Value);
|
||||
}
|
||||
|
||||
private void updateState(DrawableHitObject drawableObject, ArmedState state)
|
||||
public void Animate(ArmedState state)
|
||||
{
|
||||
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
|
||||
glow.FadeOut(400);
|
||||
|
||||
+20
-54
@@ -1,77 +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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public class SpinnerSpmCounter : Container
|
||||
public class SpinnerSpmCalculator : Component
|
||||
{
|
||||
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
|
||||
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
|
||||
|
||||
/// <summary>
|
||||
/// The resultant spins per minute value, which is updated via <see cref="SetRotation"/>.
|
||||
/// </summary>
|
||||
public IBindable<double> Result => result;
|
||||
|
||||
private readonly Bindable<double> result = new BindableDouble();
|
||||
|
||||
[Resolved]
|
||||
private DrawableHitObject drawableSpinner { get; set; }
|
||||
|
||||
private readonly OsuSpriteText spmText;
|
||||
|
||||
public SpinnerSpmCounter()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
spmText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = @"0",
|
||||
Font = OsuFont.Numeric.With(size: 24)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = @"SPINS PER MINUTE",
|
||||
Font = OsuFont.Numeric.With(size: 12),
|
||||
Y = 30
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
drawableSpinner.HitObjectApplied += resetState;
|
||||
}
|
||||
|
||||
private double spm;
|
||||
|
||||
public double SpinsPerMinute
|
||||
{
|
||||
get => spm;
|
||||
private set
|
||||
{
|
||||
if (value == spm) return;
|
||||
|
||||
spm = value;
|
||||
spmText.Text = Math.Truncate(value).ToString(@"#0");
|
||||
}
|
||||
}
|
||||
|
||||
private struct RotationRecord
|
||||
{
|
||||
public float Rotation;
|
||||
public double Time;
|
||||
}
|
||||
|
||||
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
|
||||
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
|
||||
|
||||
public void SetRotation(float currentRotation)
|
||||
{
|
||||
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
|
||||
@@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
|
||||
record = records.Dequeue();
|
||||
|
||||
SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
||||
result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
||||
}
|
||||
|
||||
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
||||
@@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
private void resetState(DrawableHitObject hitObject)
|
||||
{
|
||||
SpinsPerMinute = 0;
|
||||
result.Value = 0;
|
||||
records.Clear();
|
||||
}
|
||||
|
||||
@@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
if (drawableSpinner != null)
|
||||
drawableSpinner.HitObjectApplied -= resetState;
|
||||
}
|
||||
|
||||
private struct RotationRecord
|
||||
{
|
||||
public float Rotation;
|
||||
public double Time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
|
||||
spin = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
|
||||
|
||||
InternalChildren = new[]
|
||||
@@ -32,13 +33,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
Texture = skin.GetTexture("cursor"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Origin = centre ? Anchor.Centre : Anchor.TopLeft,
|
||||
},
|
||||
new NonPlayfieldSprite
|
||||
{
|
||||
Texture = skin.GetTexture("cursormiddle"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Origin = centre ? Anchor.Centre : Anchor.TopLeft,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Texture = skin.GetTexture("cursortrail");
|
||||
disjointTrail = skin.GetTexture("cursormiddle") == null;
|
||||
|
||||
Blending = !disjointTrail ? BlendingParameters.Additive : BlendingParameters.Inherit;
|
||||
if (disjointTrail)
|
||||
{
|
||||
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
|
||||
|
||||
TrailOrigin = centre ? Anchor.Centre : Anchor.TopLeft;
|
||||
Blending = BlendingParameters.Inherit;
|
||||
}
|
||||
else
|
||||
{
|
||||
Blending = BlendingParameters.Additive;
|
||||
}
|
||||
|
||||
if (Texture != null)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -19,7 +20,7 @@ using static osu.Game.Skinning.LegacySkinConfiguration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class LegacyMainCirclePiece : CompositeDrawable
|
||||
public class LegacyMainCirclePiece : CompositeDrawable, IMainCirclePiece
|
||||
{
|
||||
private readonly string priorityLookup;
|
||||
private readonly bool hasNumber;
|
||||
@@ -138,12 +139,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
|
||||
if (hasNumber)
|
||||
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
||||
|
||||
drawableObject.ApplyCustomUpdateState += updateState;
|
||||
updateState(drawableObject, drawableObject.State.Value);
|
||||
}
|
||||
|
||||
private void updateState(DrawableHitObject drawableObject, ArmedState state)
|
||||
public void Animate(ArmedState state)
|
||||
{
|
||||
const double legacy_fade_duration = 240;
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
protected const float SPRITE_SCALE = 0.625f;
|
||||
|
||||
private const float spm_hide_offset = 50f;
|
||||
|
||||
protected DrawableSpinner DrawableSpinner { get; private set; }
|
||||
|
||||
private Sprite spin;
|
||||
@@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private LegacySpriteText bonusCounter;
|
||||
|
||||
private Sprite spmBackground;
|
||||
private LegacySpriteText spmCounter;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
|
||||
{
|
||||
@@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = SPINNER_TOP_OFFSET + 299,
|
||||
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
||||
spmBackground = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopLeft,
|
||||
Texture = source.GetTexture("spinner-rpm"),
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Position = new Vector2(-87, 445 + spm_hide_offset),
|
||||
},
|
||||
spmCounter = new LegacySpriteText(source, LegacyFont.Score)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopRight,
|
||||
Scale = new Vector2(SPRITE_SCALE * 0.9f),
|
||||
Position = new Vector2(80, 448 + spm_hide_offset),
|
||||
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private IBindable<double> gainedBonus;
|
||||
private IBindable<double> spinsPerMinute;
|
||||
|
||||
private readonly Bindable<bool> completed = new Bindable<bool>();
|
||||
|
||||
@@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
|
||||
});
|
||||
|
||||
spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||
spinsPerMinute.BindValueChanged(spm =>
|
||||
{
|
||||
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||
}, true);
|
||||
|
||||
completed.BindValueChanged(onCompletedChanged, true);
|
||||
|
||||
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
|
||||
@@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
switch (drawableHitObject)
|
||||
{
|
||||
case DrawableSpinner d:
|
||||
double fadeOutLength = Math.Min(400, d.HitObject.Duration);
|
||||
using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn))
|
||||
{
|
||||
spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
||||
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true))
|
||||
spin.FadeOutFromOne(fadeOutLength);
|
||||
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
|
||||
|
||||
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
|
||||
spin.FadeOutFromOne(spinFadeOutLength);
|
||||
break;
|
||||
|
||||
case DrawableSpinnerTick d:
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
SliderBorderSize,
|
||||
SliderPathRadius,
|
||||
AllowSliderBallTint,
|
||||
CursorCentre,
|
||||
CursorExpand,
|
||||
CursorRotate,
|
||||
HitCircleOverlayAboveNumber,
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Batches;
|
||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||
@@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private double timeOffset;
|
||||
private float time;
|
||||
|
||||
private Anchor trailOrigin = Anchor.Centre;
|
||||
|
||||
protected Anchor TrailOrigin
|
||||
{
|
||||
get => trailOrigin;
|
||||
set
|
||||
{
|
||||
trailOrigin = value;
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
}
|
||||
|
||||
public CursorTrail()
|
||||
{
|
||||
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
|
||||
@@ -197,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||
private Vector2 size;
|
||||
|
||||
private Vector2 originPosition;
|
||||
|
||||
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
|
||||
|
||||
public TrailDrawNode(CursorTrail source)
|
||||
@@ -213,6 +228,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
size = Source.partSize;
|
||||
time = Source.time;
|
||||
|
||||
originPosition = Vector2.Zero;
|
||||
|
||||
if (Source.TrailOrigin.HasFlagFast(Anchor.x1))
|
||||
originPosition.X = 0.5f;
|
||||
else if (Source.TrailOrigin.HasFlagFast(Anchor.x2))
|
||||
originPosition.X = 1f;
|
||||
|
||||
if (Source.TrailOrigin.HasFlagFast(Anchor.y1))
|
||||
originPosition.Y = 0.5f;
|
||||
else if (Source.TrailOrigin.HasFlagFast(Anchor.y2))
|
||||
originPosition.Y = 1f;
|
||||
|
||||
Source.parts.CopyTo(parts, 0);
|
||||
}
|
||||
|
||||
@@ -237,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y + size.Y / 2),
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||
@@ -246,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y + size.Y / 2),
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||
@@ -255,7 +282,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y - size.Y / 2),
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||
@@ -264,7 +291,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y - size.Y / 2),
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||
|
||||
@@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
public OsuPlayfield()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||
|
||||
@@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
Add(cursorScaleContainer = new Container
|
||||
{
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
|
||||
});
|
||||
}
|
||||
@@ -43,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
base.PopIn();
|
||||
|
||||
GameplayCursor.ActiveCursor.Hide();
|
||||
cursorScaleContainer.MoveTo(GameplayCursor.ActiveCursor.Position);
|
||||
cursorScaleContainer.Position = ToLocalSpace(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre);
|
||||
clickToResumeCursor.Appear();
|
||||
|
||||
if (localCursorContainer == null)
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
SetContents(() => new TaikoInputManager(new RulesetInfo { ID = 1 })
|
||||
SetContents(() => new TaikoInputManager(new TaikoRuleset().RulesetInfo)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
StartTime = 400,
|
||||
Major = true
|
||||
}), null));
|
||||
})));
|
||||
AddHitObject(barLine);
|
||||
RemoveHitObject(barLine);
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
StartTime = 200,
|
||||
Major = false
|
||||
}), null));
|
||||
})));
|
||||
AddHitObject(barLine);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Duration = 500,
|
||||
IsStrong = false,
|
||||
TickRate = 2
|
||||
}), null));
|
||||
})));
|
||||
|
||||
AddHitObject(drumRoll);
|
||||
RemoveHitObject(drumRoll);
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Duration = 400,
|
||||
IsStrong = true,
|
||||
TickRate = 16
|
||||
}), null));
|
||||
})));
|
||||
|
||||
AddHitObject(drumRoll);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Type = HitType.Rim,
|
||||
IsStrong = false,
|
||||
StartTime = 300
|
||||
}), null));
|
||||
})));
|
||||
|
||||
AddHitObject(hit);
|
||||
RemoveHitObject(hit);
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Type = HitType.Centre,
|
||||
IsStrong = true,
|
||||
StartTime = 500
|
||||
}), null));
|
||||
})));
|
||||
|
||||
AddHitObject(hit);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (PlacementActive)
|
||||
if (PlacementActive == PlacementState.Active)
|
||||
{
|
||||
if (result.Time is double dragTime)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModClassic : ModClassic
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Taiko.Replays
|
||||
|
||||
bool hitButton = true;
|
||||
|
||||
Frames.Add(new TaikoReplayFrame(-100000));
|
||||
Frames.Add(new TaikoReplayFrame(Beatmap.HitObjects[0].StartTime - 1000));
|
||||
|
||||
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
|
||||
|
||||
@@ -135,6 +135,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
new TaikoModRandom(),
|
||||
new TaikoModDifficultyAdjust(),
|
||||
new TaikoModClassic(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
||||
@@ -168,6 +168,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||
|
||||
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||
|
||||
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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 Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckAudioQualityTest
|
||||
{
|
||||
private CheckAudioQuality check;
|
||||
private IBeatmap beatmap;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckAudioQuality();
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissing()
|
||||
{
|
||||
// While this is a problem, it is out of scope for this check and is caught by a different one.
|
||||
beatmap.Metadata.AudioFile = null;
|
||||
|
||||
var mock = new Mock<IWorkingBeatmap>();
|
||||
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
|
||||
mock.SetupGet(w => w.Track).Returns((Track)null);
|
||||
|
||||
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAcceptable()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(192);
|
||||
|
||||
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullBitrate()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(null);
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroBitrate()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(0);
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooHighBitrate()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(320);
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooLowBitrate()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(64);
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the mock of the working beatmap with the given audio properties.
|
||||
/// </summary>
|
||||
/// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param>
|
||||
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate)
|
||||
{
|
||||
var mockTrack = new Mock<Track>();
|
||||
mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate);
|
||||
|
||||
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
|
||||
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
|
||||
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object);
|
||||
|
||||
return mockWorkingBeatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using FileInfo = osu.Game.IO.FileInfo;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckBackgroundQualityTest
|
||||
{
|
||||
private CheckBackgroundQuality check;
|
||||
private IBeatmap beatmap;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckBackgroundQuality();
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Files = new List<BeatmapSetFileInfo>(new[]
|
||||
{
|
||||
new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = "abc123.jpg",
|
||||
FileInfo = new FileInfo
|
||||
{
|
||||
Hash = "abcdef"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissing()
|
||||
{
|
||||
// While this is a problem, it is out of scope for this check and is caught by a different one.
|
||||
beatmap.Metadata.BackgroundFile = null;
|
||||
var mock = getMockWorkingBeatmap(null, System.Array.Empty<byte>());
|
||||
|
||||
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAcceptable()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(new Texture(1920, 1080));
|
||||
|
||||
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooHighResolution()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(new Texture(3840, 2160));
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooHighResolution);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLowResolution()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(new Texture(640, 480));
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateLowResolution);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooLowResolution()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(new Texture(100, 100));
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooLowResolution);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooUncompressed()
|
||||
{
|
||||
var mock = getMockWorkingBeatmap(new Texture(1920, 1080), new byte[1024 * 1024 * 3]);
|
||||
|
||||
var issues = check.Run(beatmap, mock.Object).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the mock of the working beatmap with the given background and filesize.
|
||||
/// </summary>
|
||||
/// <param name="background">The texture of the background.</param>
|
||||
/// <param name="fileBytes">The bytes that represent the background file.</param>
|
||||
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] byte[] fileBytes = null)
|
||||
{
|
||||
var stream = new MemoryStream(fileBytes ?? new byte[1024 * 1024]);
|
||||
|
||||
var mock = new Mock<IWorkingBeatmap>();
|
||||
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
|
||||
mock.SetupGet(w => w.Background).Returns(background);
|
||||
mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream);
|
||||
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckFilePresenceTest
|
||||
{
|
||||
private CheckBackgroundPresence check;
|
||||
private IBeatmap beatmap;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckBackgroundPresence();
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Files = new List<BeatmapSetFileInfo>(new[]
|
||||
{
|
||||
new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = "abc123.jpg",
|
||||
FileInfo = new FileInfo { Hash = "abcdef" }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundSetAndInFiles()
|
||||
{
|
||||
Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundSetAndNotInFiles()
|
||||
{
|
||||
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
|
||||
|
||||
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateDoesNotExist);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundNotSet()
|
||||
{
|
||||
beatmap.Metadata.BackgroundFile = null;
|
||||
|
||||
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateNoneSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneGameplayClockContainer : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestStartThenElapsedTime()
|
||||
{
|
||||
GameplayClockContainer gcc = null;
|
||||
|
||||
AddStep("create container", () =>
|
||||
{
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gcc = new GameplayClockContainer(working, 0));
|
||||
});
|
||||
|
||||
AddStep("start track", () => gcc.Start());
|
||||
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Gameplay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneMasterGameplayClockContainer : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestStartThenElapsedTime()
|
||||
{
|
||||
GameplayClockContainer gcc = null;
|
||||
|
||||
AddStep("create container", () =>
|
||||
{
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gcc = new MasterGameplayClockContainer(working, 0));
|
||||
});
|
||||
|
||||
AddStep("start clock", () => gcc.Start());
|
||||
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestElapseThenReset()
|
||||
{
|
||||
GameplayClockContainer gcc = null;
|
||||
|
||||
AddStep("create container", () =>
|
||||
{
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gcc = new MasterGameplayClockContainer(working, 0));
|
||||
});
|
||||
|
||||
AddStep("start clock", () => gcc.Start());
|
||||
AddUntilStep("current time greater 2000", () => gcc.GameplayClock.CurrentTime > 2000);
|
||||
|
||||
double timeAtReset = 0;
|
||||
AddStep("reset clock", () =>
|
||||
{
|
||||
timeAtReset = gcc.GameplayClock.CurrentTime;
|
||||
gcc.Reset();
|
||||
});
|
||||
|
||||
AddAssert("current time < time at reset", () => gcc.GameplayClock.CurrentTime < timeAtReset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
@@ -67,17 +68,47 @@ namespace osu.Game.Tests.Gameplay
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gameplayContainer = new GameplayClockContainer(working, 0));
|
||||
|
||||
gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
|
||||
Add(gameplayContainer = new MasterGameplayClockContainer(working, 0)
|
||||
{
|
||||
Clock = gameplayContainer.GameplayClock
|
||||
IsPaused = { Value = true },
|
||||
Child = new FrameStabilityContainer
|
||||
{
|
||||
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("reset clock", () => gameplayContainer.Start());
|
||||
|
||||
AddUntilStep("sample played", () => sample.RequestedPlaying);
|
||||
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSampleHasLifetimeEndWithInitialClockTime()
|
||||
{
|
||||
GameplayClockContainer gameplayContainer = null;
|
||||
DrawableStoryboardSample sample = null;
|
||||
|
||||
AddStep("create container", () =>
|
||||
{
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.LoadTrack();
|
||||
|
||||
Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true)
|
||||
{
|
||||
IsPaused = { Value = true },
|
||||
Child = new FrameStabilityContainer
|
||||
{
|
||||
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("start time", () => gameplayContainer.Start());
|
||||
|
||||
AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue);
|
||||
AddUntilStep("sample not played", () => !sample.RequestedPlaying);
|
||||
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
|
||||
}
|
||||
|
||||
[TestCase(typeof(OsuModDoubleTime), 1.5)]
|
||||
@@ -114,7 +145,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
|
||||
|
||||
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)
|
||||
Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
|
||||
{
|
||||
Child = beatmapSkinSourceContainer
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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.Online.API;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Tests.Mods
|
||||
{
|
||||
[TestFixture]
|
||||
public class ModSettingsEqualityComparison
|
||||
{
|
||||
[Test]
|
||||
public void Test()
|
||||
{
|
||||
var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
|
||||
var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
|
||||
var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
|
||||
var apiMod1 = new APIMod(mod1);
|
||||
var apiMod2 = new APIMod(mod2);
|
||||
var apiMod3 = new APIMod(mod3);
|
||||
|
||||
Assert.That(mod1, Is.Not.EqualTo(mod2));
|
||||
Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
|
||||
|
||||
Assert.That(mod2, Is.EqualTo(mod2));
|
||||
Assert.That(apiMod2, Is.EqualTo(apiMod2));
|
||||
|
||||
Assert.That(mod2, Is.EqualTo(mod3));
|
||||
Assert.That(apiMod2, Is.EqualTo(apiMod3));
|
||||
|
||||
Assert.That(mod3, Is.EqualTo(mod2));
|
||||
Assert.That(apiMod3, Is.EqualTo(apiMod2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
public override string Name => nameof(ModA);
|
||||
public override string Acronym => nameof(ModA);
|
||||
public override string Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) };
|
||||
@@ -152,6 +153,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
private class ModB : Mod
|
||||
{
|
||||
public override string Name => nameof(ModB);
|
||||
public override string Description => string.Empty;
|
||||
public override string Acronym => nameof(ModB);
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
@@ -162,6 +164,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
public override string Name => nameof(ModC);
|
||||
public override string Acronym => nameof(ModC);
|
||||
public override string Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
|
||||
@@ -169,6 +172,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
public override string Name => $"Incompatible With {nameof(ModA)}";
|
||||
public override string Acronym => $"Incompatible With {nameof(ModA)}";
|
||||
public override string Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModA) };
|
||||
@@ -187,6 +191,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
|
||||
public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
|
||||
public override string Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
|
||||
|
||||
@@ -20,27 +20,14 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
handler = new TestInputHandler(replay = new Replay
|
||||
{
|
||||
Frames = new List<ReplayFrame>
|
||||
{
|
||||
new TestReplayFrame(0),
|
||||
new TestReplayFrame(1000),
|
||||
new TestReplayFrame(2000),
|
||||
new TestReplayFrame(3000, true),
|
||||
new TestReplayFrame(4000, true),
|
||||
new TestReplayFrame(5000, true),
|
||||
new TestReplayFrame(7000, true),
|
||||
new TestReplayFrame(8000),
|
||||
}
|
||||
HasReceivedAllFrames = false
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalPlayback()
|
||||
{
|
||||
Assert.IsNull(handler.CurrentFrame);
|
||||
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
setReplayFrames();
|
||||
|
||||
setTime(0, 0);
|
||||
confirmCurrentFrame(0);
|
||||
@@ -107,6 +94,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestIntroTime()
|
||||
{
|
||||
setReplayFrames();
|
||||
|
||||
setTime(-1000, -1000);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
@@ -123,6 +112,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestBasicRewind()
|
||||
{
|
||||
setReplayFrames();
|
||||
|
||||
setTime(2800, 0);
|
||||
setTime(2800, 1000);
|
||||
setTime(2800, 2000);
|
||||
@@ -133,34 +124,35 @@ namespace osu.Game.Tests.NonVisual
|
||||
// pivot without crossing a frame boundary
|
||||
setTime(2700, 2700);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
confirmNextFrame(3);
|
||||
|
||||
// cross current frame boundary; should not yet update frame
|
||||
setTime(1980, 1980);
|
||||
// cross current frame boundary
|
||||
setTime(1980, 2000);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
confirmNextFrame(3);
|
||||
|
||||
setTime(1200, 1200);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(2);
|
||||
|
||||
// ensure each frame plays out until start
|
||||
setTime(-500, 1000);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(0);
|
||||
confirmNextFrame(2);
|
||||
|
||||
setTime(-500, 0);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(null);
|
||||
confirmNextFrame(1);
|
||||
|
||||
setTime(-500, -500);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(null);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindInsideImportantSection()
|
||||
{
|
||||
setReplayFrames();
|
||||
fastForwardToPoint(3000);
|
||||
|
||||
setTime(4000, 4000);
|
||||
@@ -168,12 +160,12 @@ namespace osu.Game.Tests.NonVisual
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(3);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(3);
|
||||
@@ -187,46 +179,127 @@ namespace osu.Game.Tests.NonVisual
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(4000, null);
|
||||
setTime(4000, 4000);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(3);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
confirmNextFrame(4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindOutOfImportantSection()
|
||||
{
|
||||
setReplayFrames();
|
||||
fastForwardToPoint(3500);
|
||||
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3200, null);
|
||||
// next frame doesn't change even though direction reversed, because of important section.
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3000, null);
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(2800, 2800);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReplayStreaming()
|
||||
{
|
||||
// no frames are arrived yet
|
||||
setTime(0, null);
|
||||
setTime(1000, null);
|
||||
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting for the first frame");
|
||||
|
||||
replay.Frames.Add(new TestReplayFrame(0));
|
||||
replay.Frames.Add(new TestReplayFrame(1000));
|
||||
|
||||
// should always play from beginning
|
||||
setTime(1000, 0);
|
||||
confirmCurrentFrame(0);
|
||||
Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet");
|
||||
setTime(1000, 1000);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(null);
|
||||
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting");
|
||||
|
||||
// cannot seek beyond the last frame
|
||||
setTime(1500, null);
|
||||
confirmCurrentFrame(1);
|
||||
|
||||
setTime(-100, 0);
|
||||
confirmCurrentFrame(0);
|
||||
|
||||
// can seek to the point before the first frame, however
|
||||
setTime(-100, -100);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
|
||||
fastForwardToPoint(1000);
|
||||
setTime(3000, null);
|
||||
replay.Frames.Add(new TestReplayFrame(2000));
|
||||
confirmCurrentFrame(1);
|
||||
setTime(1000, 1000);
|
||||
setTime(3000, 2000);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleFramesSameTime()
|
||||
{
|
||||
replay.Frames.Add(new TestReplayFrame(0));
|
||||
replay.Frames.Add(new TestReplayFrame(0));
|
||||
replay.Frames.Add(new TestReplayFrame(1000));
|
||||
replay.Frames.Add(new TestReplayFrame(1000));
|
||||
replay.Frames.Add(new TestReplayFrame(2000));
|
||||
|
||||
// forward direction is prioritized when multiple frames have the same time.
|
||||
setTime(0, 0);
|
||||
setTime(0, 0);
|
||||
|
||||
setTime(2000, 1000);
|
||||
setTime(2000, 1000);
|
||||
|
||||
setTime(1000, 1000);
|
||||
setTime(1000, 1000);
|
||||
setTime(-100, 1000);
|
||||
setTime(-100, 0);
|
||||
setTime(-100, 0);
|
||||
setTime(-100, -100);
|
||||
}
|
||||
|
||||
private void setReplayFrames()
|
||||
{
|
||||
replay.Frames = new List<ReplayFrame>
|
||||
{
|
||||
new TestReplayFrame(0),
|
||||
new TestReplayFrame(1000),
|
||||
new TestReplayFrame(2000),
|
||||
new TestReplayFrame(3000, true),
|
||||
new TestReplayFrame(4000, true),
|
||||
new TestReplayFrame(5000, true),
|
||||
new TestReplayFrame(7000, true),
|
||||
new TestReplayFrame(8000),
|
||||
};
|
||||
replay.HasReceivedAllFrames = true;
|
||||
}
|
||||
|
||||
private void fastForwardToPoint(double destination)
|
||||
{
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
if (handler.SetFrameFromTime(destination) == null)
|
||||
var time = handler.SetFrameFromTime(destination);
|
||||
if (time == null || time == destination)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -235,33 +308,17 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
private void setTime(double set, double? expect)
|
||||
{
|
||||
Assert.AreEqual(expect, handler.SetFrameFromTime(set));
|
||||
Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value");
|
||||
}
|
||||
|
||||
private void confirmCurrentFrame(int? frame)
|
||||
{
|
||||
if (frame.HasValue)
|
||||
{
|
||||
Assert.IsNotNull(handler.CurrentFrame);
|
||||
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(handler.CurrentFrame);
|
||||
}
|
||||
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame");
|
||||
}
|
||||
|
||||
private void confirmNextFrame(int? frame)
|
||||
{
|
||||
if (frame.HasValue)
|
||||
{
|
||||
Assert.IsNotNull(handler.NextFrame);
|
||||
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(handler.NextFrame);
|
||||
}
|
||||
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame");
|
||||
}
|
||||
|
||||
private class TestReplayFrame : ReplayFrame
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user