Merge remote-tracking branch 'refs/remotes/ppy/master' into remove-comments-page

This commit is contained in:
Andrei Zavatski 2020-07-30 00:39:32 +03:00
commit 929b10036e
214 changed files with 4074 additions and 1564 deletions

View File

@ -5,6 +5,6 @@
"version": "3.1.100" "version": "3.1.100"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.50" "Microsoft.Build.Traversal": "2.0.52"
} }
} }

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.622.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.727.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.714.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.723.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -16,6 +16,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Desktop.Windows;
namespace osu.Desktop namespace osu.Desktop
{ {
@ -98,6 +99,9 @@ namespace osu.Desktop
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
LoadComponentAsync(new DiscordRichPresence(), Add); LoadComponentAsync(new DiscordRichPresence(), Add);
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
} }
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Configuration;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
private Bindable<bool> allowScreenSuspension;
private Bindable<bool> disableWinKey;
private GameHost host;
[BackgroundDependencyLoader]
private void load(GameHost host, OsuConfigManager config)
{
this.host = host;
allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
allowScreenSuspension.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
}
private void updateBlocking()
{
bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);
else
host.InputThread.Scheduler.Add(WindowsKey.Enable);
}
}
}

View File

@ -0,0 +1,80 @@
// 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.Runtime.InteropServices;
namespace osu.Desktop.Windows
{
internal class WindowsKey
{
private delegate int LowLevelKeyboardProcDelegate(int nCode, int wParam, ref KdDllHookStruct lParam);
private static bool isBlocked;
private const int wh_keyboard_ll = 13;
private const int wm_keydown = 256;
private const int wm_syskeyup = 261;
//Resharper disable once NotAccessedField.Local
private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC
private static IntPtr keyHook;
[StructLayout(LayoutKind.Explicit)]
private readonly struct KdDllHookStruct
{
[FieldOffset(0)]
public readonly int VkCode;
[FieldOffset(8)]
public readonly int Flags;
}
private static int lowLevelKeyboardProc(int nCode, int wParam, ref KdDllHookStruct lParam)
{
if (wParam >= wm_keydown && wParam <= wm_syskeyup)
{
switch (lParam.VkCode)
{
case 0x5B: // left windows key
case 0x5C: // right windows key
return 1;
}
}
return callNextHookEx(0, nCode, wParam, ref lParam);
}
internal static void Disable()
{
if (keyHook != IntPtr.Zero || isBlocked)
return;
keyHook = setWindowsHookEx(wh_keyboard_ll, (keyboardHookDelegate = lowLevelKeyboardProc), Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
isBlocked = true;
}
internal static void Enable()
{
if (keyHook == IntPtr.Zero || !isBlocked)
return;
keyHook = unhookWindowsHookEx(keyHook);
keyboardHookDelegate = null;
keyHook = IntPtr.Zero;
isBlocked = false;
}
[DllImport(@"user32.dll", EntryPoint = @"SetWindowsHookExA")]
private static extern IntPtr setWindowsHookEx(int idHook, LowLevelKeyboardProcDelegate lpfn, IntPtr hMod, int dwThreadId);
[DllImport(@"user32.dll", EntryPoint = @"UnhookWindowsHookEx")]
private static extern IntPtr unhookWindowsHookEx(IntPtr hHook);
[DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")]
private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam);
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss); public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
// We only care about testing misses, hits are tested via JuiceStream // We only care about testing misses, hits are tested via JuiceStream
[TestCase(true)] [TestCase(false)]
public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
} }
} }

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchModHidden : ModTestScene
{
[BackgroundDependencyLoader]
private void load()
{
LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false);
}
[Test]
public void TestJuiceStream()
{
CreateModTest(new ModTestData
{
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new JuiceStream
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
X = CatchPlayfield.WIDTH / 2
}
}
},
Mod = new CatchModHidden(),
PassCondition = () => Player.Results.Count > 0
&& Player.ChildrenOfType<DrawableJuiceStream>().Single().Alpha > 0
&& Player.ChildrenOfType<DrawableFruit>().Last().Alpha > 0
});
}
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
private RulesetInfo catchRuleset; private RulesetInfo catchRuleset;
[Resolved]
private OsuConfigManager config { get; set; }
private Catcher catcher => this.ChildrenOfType<CatcherArea>().First().MovableCatcher;
public TestSceneCatcherArea() public TestSceneCatcherArea()
{ {
AddSliderStep<float>("CircleSize", 0, 8, 5, createCatcher); AddSliderStep<float>("CircleSize", 0, 8, 5, createCatcher);
@ -34,24 +40,43 @@ namespace osu.Game.Rulesets.Catch.Tests
AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false) AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
{ {
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X X = catcher.X
}), 20); }), 20);
AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false) AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
{ {
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X, X = catcher.X,
LastInCombo = true, LastInCombo = true,
}), 20); }), 20);
AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true) AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
{ {
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X, X = catcher.X
}), 20); }), 20);
AddRepeatStep("miss fruit", () => catchFruit(new Fruit AddRepeatStep("miss fruit", () => catchFruit(new Fruit
{ {
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X + 100, X = catcher.X + 100,
LastInCombo = true, LastInCombo = true,
}, true), 20); }, true), 20);
} }
[TestCase(true)]
[TestCase(false)]
public void TestHitLighting(bool enable)
{
AddStep("create catcher", () => createCatcher(5));
AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable));
AddStep("catch fruit", () => catchFruit(new TestFruit(false)
{
X = catcher.X
}));
AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
{
X = catcher.X,
LastInCombo = true
}));
AddAssert("check hit explosion", () => catcher.ChildrenOfType<HitExplosion>().Any() == enable);
}
private void catchFruit(Fruit fruit, bool miss = false) private void catchFruit(Fruit fruit, bool miss = false)
{ {
this.ChildrenOfType<CatcherArea>().ForEach(area => this.ChildrenOfType<CatcherArea>().ForEach(area =>

View File

@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (mods.Any(m => m is ModHidden)) if (mods.Any(m => m is ModHidden))
{ {
value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
// Hiddens gives almost nothing on max approach rate, and more the lower it is // Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0) if (approachRate <= 10.0)
value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10 value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10

View File

@ -1,17 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModPerfect : ModPerfect public class CatchModPerfect : ModPerfect
{ {
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> !(result.Judgement is CatchBananaJudgement)
&& base.FailCondition(healthProcessor, result);
} }
} }

View File

@ -35,18 +35,15 @@ namespace osu.Game.Rulesets.Catch.Replays
} }
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
if (!Position.HasValue) return new List<IInput>(); if (!Position.HasValue) return;
return new List<IInput> inputs.Add(new CatchReplayState
{ {
new CatchReplayState PressedActions = CurrentFrame?.Actions ?? new List<CatchAction>(),
{ CatcherX = Position.Value
PressedActions = CurrentFrame?.Actions ?? new List<CatchAction>(), });
CatcherX = Position.Value
},
};
} }
public class CatchReplayState : ReplayState<CatchAction> public class CatchReplayState : ReplayState<CatchAction>

View File

@ -35,22 +35,25 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation) public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
{ {
Container explodingFruitContainer; var explodingFruitContainer = new Container
InternalChildren = new Drawable[]
{ {
explodingFruitContainer = new Container RelativeSizeAxes = Axes.Both,
{ };
RelativeSizeAxes = Axes.Both,
}, CatcherArea = new CatcherArea(difficulty)
CatcherArea = new CatcherArea(difficulty) {
{ CreateDrawableRepresentation = createDrawableRepresentation,
CreateDrawableRepresentation = createDrawableRepresentation, ExplodingFruitTarget = explodingFruitContainer,
ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft,
Origin = Anchor.TopLeft, };
},
HitObjectContainer InternalChildren = new[]
{
explodingFruitContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer,
CatcherArea
}; };
} }

View File

@ -5,12 +5,14 @@ using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Catch.Skinning;
@ -46,6 +48,12 @@ namespace osu.Game.Rulesets.Catch.UI
public Container ExplodingFruitTarget; public Container ExplodingFruitTarget;
private Container<DrawableHitObject> caughtFruitContainer { get; } = new Container<DrawableHitObject>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
};
[NotNull] [NotNull]
private readonly Container trailsTarget; private readonly Container trailsTarget;
@ -83,8 +91,6 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private readonly float catchWidth; private readonly float catchWidth;
private Container<DrawableHitObject> caughtFruit;
private CatcherSprite catcherIdle; private CatcherSprite catcherIdle;
private CatcherSprite catcherKiai; private CatcherSprite catcherKiai;
private CatcherSprite catcherFail; private CatcherSprite catcherFail;
@ -99,6 +105,7 @@ namespace osu.Game.Rulesets.Catch.UI
private double hyperDashModifier = 1; private double hyperDashModifier = 1;
private int hyperDashDirection; private int hyperDashDirection;
private float hyperDashTargetPosition; private float hyperDashTargetPosition;
private Bindable<bool> hitLighting;
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
{ {
@ -114,15 +121,13 @@ namespace osu.Game.Rulesets.Catch.UI
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
caughtFruit = new Container<DrawableHitObject> caughtFruitContainer,
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -145,6 +150,11 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher(); updateCatcher();
} }
/// <summary>
/// Creates proxied content to be displayed beneath hitobjects.
/// </summary>
public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy();
/// <summary> /// <summary>
/// Calculates the scale of the catcher based off the provided beatmap difficulty. /// Calculates the scale of the catcher based off the provided beatmap difficulty.
/// </summary> /// </summary>
@ -176,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI
const float allowance = 10; const float allowance = 10;
while (caughtFruit.Any(f => while (caughtFruitContainer.Any(f =>
f.LifetimeEnd == double.MaxValue && f.LifetimeEnd == double.MaxValue &&
Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
{ {
@ -187,13 +197,16 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
caughtFruit.Add(fruit); caughtFruitContainer.Add(fruit);
AddInternal(new HitExplosion(fruit) if (hitLighting.Value)
{ {
X = fruit.X, AddInternal(new HitExplosion(fruit)
Scale = new Vector2(fruit.HitObject.Scale) {
}); X = fruit.X,
Scale = new Vector2(fruit.HitObject.Scale)
});
}
} }
/// <summary> /// <summary>
@ -342,7 +355,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
public void Drop() public void Drop()
{ {
foreach (var f in caughtFruit.ToArray()) foreach (var f in caughtFruitContainer.ToArray())
Drop(f); Drop(f);
} }
@ -351,7 +364,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
public void Explode() public void Explode()
{ {
foreach (var f in caughtFruit.ToArray()) foreach (var f in caughtFruitContainer.ToArray())
Explode(f); Explode(f);
} }
@ -450,9 +463,9 @@ namespace osu.Game.Rulesets.Catch.UI
if (ExplodingFruitTarget != null) if (ExplodingFruitTarget != null)
{ {
fruit.Anchor = Anchor.TopLeft; fruit.Anchor = Anchor.TopLeft;
fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
if (!caughtFruit.Remove(fruit)) if (!caughtFruitContainer.Remove(fruit))
// we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
return; return;

View File

@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.UI
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation; public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
public readonly Catcher MovableCatcher;
public Container ExplodingFruitTarget public Container ExplodingFruitTarget
{ {
set => MovableCatcher.ExplodingFruitTarget = value; set => MovableCatcher.ExplodingFruitTarget = value;
@ -104,7 +106,5 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null) if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value; MovableCatcher.X = state.CatcherX.Value;
} }
protected internal readonly Catcher MovableCatcher;
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -10,6 +11,8 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -236,6 +239,53 @@ namespace osu.Game.Rulesets.Mania.Tests
assertTailJudgement(HitResult.Meh); assertTailJudgement(HitResult.Meh);
} }
[Test]
public void TestMissReleaseAndHitSecondRelease()
{
var windows = new ManiaHitWindows();
windows.SetDifficulty(10);
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = 1000,
Duration = 500,
Column = 0,
},
new HoldNote
{
StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
Duration = 500,
Column = 0,
},
},
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty
{
SliderTickRate = 4,
OverallDifficulty = 10,
},
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
}, beatmap);
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type == HitResult.Miss));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type == HitResult.Perfect));
}
private void assertHeadJudgement(HitResult result) private void assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
@ -250,11 +300,11 @@ namespace osu.Game.Rulesets.Mania.Tests
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer;
private void performTest(List<ReplayFrame> frames) private void performTest(List<ReplayFrame> frames, Beatmap<ManiaHitObject> beatmap = null)
{ {
AddStep("load player", () => if (beatmap == null)
{ {
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<ManiaHitObject> beatmap = new Beatmap<ManiaHitObject>
{ {
HitObjects = HitObjects =
{ {
@ -270,9 +320,14 @@ namespace osu.Game.Rulesets.Mania.Tests
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 }, BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo Ruleset = new ManiaRuleset().RulesetInfo
}, },
}); };
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
}
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestScenePlayfieldCoveringContainer : OsuTestScene
{
private readonly ScrollingTestContainer scrollingContainer;
private readonly PlayfieldCoveringWrapper cover;
public TestScenePlayfieldCoveringContainer()
{
Child = scrollingContainer = new ScrollingTestContainer(ScrollingDirection.Down)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(300, 500),
Child = cover = new PlayfieldCoveringWrapper(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Orange
})
{
RelativeSizeAxes = Axes.Both,
}
};
}
[Test]
public void TestScrollingDownwards()
{
AddStep("set down scroll", () => scrollingContainer.Direction = ScrollingDirection.Down);
AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
}
[Test]
public void TestScrollingUpwards()
{
AddStep("set up scroll", () => scrollingContainer.Direction = ScrollingDirection.Up);
AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
return 300; return 300;
case HitResult.Perfect: case HitResult.Perfect:
return 320; return 350;
} }
} }
} }

View File

@ -1,23 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModFadeIn : Mod public class ManiaModFadeIn : ManiaModHidden
{ {
public override string Name => "Fade In"; public override string Name => "Fade In";
public override string Acronym => "FI"; public override string Acronym => "FI";
public override IconUsage? Icon => OsuIcon.ModHidden; public override IconUsage? Icon => OsuIcon.ModHidden;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Keys appear out of nowhere!"; public override string Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
public override bool Ranked => true; protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) };
} }
} }

View File

@ -2,15 +2,44 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModHidden : ModHidden public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset<ManiaHitObject>
{ {
public override string Description => @"Keys fade out before you hit them!"; public override string Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) }; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) };
/// <summary>
/// The direction in which the cover should expand.
/// </summary>
protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
{
ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
{
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
Container hocParent = (Container)hoc.Parent;
hocParent.Remove(hoc);
hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
{
c.RelativeSizeAxes = Axes.Both;
c.Direction = ExpandDirection;
c.Coverage = 0.5f;
}));
}
}
} }
} }

View File

@ -167,6 +167,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value) if (action != Action.Value)
return false; return false;
// The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
// But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
// Note: Unlike below, we use the tail's start time to determine the time offset.
if (Time.Current > Tail.HitObject.StartTime && !Tail.HitObject.HitWindows.CanBeHit(Time.Current - Tail.HitObject.StartTime))
return false;
beginHoldAt(Time.Current - Head.HitObject.StartTime); beginHoldAt(Time.Current - Head.HitObject.StartTime);
Head.UpdateResult(); Head.UpdateResult();

View File

@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Mania.Replays
protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any();
public override List<IInput> GetPendingInputs() => new List<IInput> { new ReplayState<ManiaAction> { PressedActions = CurrentFrame?.Actions ?? new List<ManiaAction>() } }; public override void CollectPendingInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<ManiaAction> { PressedActions = CurrentFrame?.Actions ?? new List<ManiaAction>() });
}
} }
} }

View File

@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Mania.Scoring
{ {
internal class ManiaScoreProcessor : ScoreProcessor internal class ManiaScoreProcessor : ScoreProcessor
{ {
protected override double DefaultAccuracyPortion => 0.95;
protected override double DefaultComboPortion => 0.05;
public override HitWindows CreateHitWindows() => new ManiaHitWindows(); public override HitWindows CreateHitWindows() => new ManiaHitWindows();
} }
} }

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L"; ?? $"mania-note{FallbackColumnIndex}L";
sprite = skin.GetAnimation(imageName, true, true).With(d => sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
{ {
if (d == null) if (d == null)
return; return;

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
string noteImage = GetColumnSkinConfig<string>(skin, lookup)?.Value string noteImage = GetColumnSkinConfig<string>(skin, lookup)?.Value
?? $"mania-note{FallbackColumnIndex}{suffix}"; ?? $"mania-note{FallbackColumnIndex}{suffix}";
return skin.GetTexture(noteImage); return skin.GetTexture(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge);
} }
} }
} }

View File

@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>(); public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>();
private readonly ColumnHitObjectArea hitObjectArea; public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer; internal readonly Container TopLevelContainer;
public Container UnderlayElements => hitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
public Column(int index) public Column(int index)
{ {
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(), background.CreateProxy(),
hitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea()) new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
}; };
TopLevelContainer.Add(hitObjectArea.Explosions.CreateProxy()); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
} }
public override Axes RelativeSizeAxes => Axes.Y; public override Axes RelativeSizeAxes => Axes.Y;
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}; };
hitObjectArea.Explosions.Add(explosion); HitObjectArea.Explosions.Add(explosion);
explosion.Delay(200).Expire(true); explosion.Delay(200).Expire(true);
} }

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -14,12 +15,14 @@ namespace osu.Game.Rulesets.Mania.UI.Components
public class HitObjectArea : SkinReloadableDrawable public class HitObjectArea : SkinReloadableDrawable
{ {
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
public readonly HitObjectContainer HitObjectContainer;
public HitObjectArea(HitObjectContainer hitObjectContainer) public HitObjectArea(HitObjectContainer hitObjectContainer)
{ {
InternalChildren = new[] InternalChild = new Container
{ {
hitObjectContainer, RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer = hitObjectContainer
}; };
} }

View File

@ -0,0 +1,133 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// A <see cref="Container"/> that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield.
/// </summary>
public class PlayfieldCoveringWrapper : CompositeDrawable
{
/// <summary>
/// The complete cover, including gradient and fill.
/// </summary>
private readonly Drawable cover;
/// <summary>
/// The gradient portion of the cover.
/// </summary>
private readonly Box gradient;
/// <summary>
/// The fully-opaque portion of the cover.
/// </summary>
private readonly Box filled;
private readonly IBindable<ScrollingDirection> scrollDirection = new Bindable<ScrollingDirection>();
public PlayfieldCoveringWrapper(Drawable content)
{
InternalChild = new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
content,
cover = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Blending = new BlendingParameters
{
// Don't change the destination colour.
RGBEquation = BlendingEquation.Add,
Source = BlendingType.Zero,
Destination = BlendingType.One,
// Subtract the cover's alpha from the destination (points with alpha 1 should make the destination completely transparent).
AlphaEquation = BlendingEquation.Add,
SourceAlpha = BlendingType.Zero,
DestinationAlpha = BlendingType.OneMinusSrcAlpha
},
Children = new Drawable[]
{
gradient = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Height = 0.25f,
Colour = ColourInfo.GradientVertical(
Color4.White.Opacity(0f),
Color4.White.Opacity(1f)
)
},
filled = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
scrollDirection.BindTo(scrollingInfo.Direction);
scrollDirection.BindValueChanged(onScrollDirectionChanged, true);
}
private void onScrollDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
=> cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f;
/// <summary>
/// The relative area that should be completely covered. This does not include the fade.
/// </summary>
public float Coverage
{
set
{
filled.Height = value;
gradient.Y = -value;
}
}
/// <summary>
/// The direction in which the cover expands.
/// </summary>
public CoverExpandDirection Direction
{
set => cover.Scale = value == CoverExpandDirection.AlongScroll ? Vector2.One : new Vector2(1, -1);
}
}
public enum CoverExpandDirection
{
/// <summary>
/// The cover expands along the scrolling direction.
/// </summary>
AlongScroll,
/// <summary>
/// The cover expands against the scrolling direction.
/// </summary>
AgainstScroll
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Audio; using osu.Game.Audio;
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
public Texture GetTexture(string componentName) public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
switch (componentName) switch (componentName)
{ {

View File

@ -4,62 +4,109 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneDrawableJudgement : OsuSkinnableTestScene public class TestSceneDrawableJudgement : OsuSkinnableTestScene
{ {
[Resolved]
private OsuConfigManager config { get; set; }
private readonly List<DrawablePool<TestDrawableOsuJudgement>> pools;
public TestSceneDrawableJudgement() public TestSceneDrawableJudgement()
{ {
var pools = new List<DrawablePool<DrawableOsuJudgement>>(); pools = new List<DrawablePool<TestDrawableOsuJudgement>>();
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Skip(1)) foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Skip(1))
showResult(result);
}
[Test]
public void TestHitLightingDisabled()
{
AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false));
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
AddAssert("judgement body immediately visible",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha == 1));
AddAssert("hit lighting hidden",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha == 0));
}
[Test]
public void TestHitLightingEnabled()
{
AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true));
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
AddAssert("judgement body not immediately visible",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
AddAssert("hit lighting shown",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha > 0));
}
private void showResult(HitResult result)
{
AddStep("Show " + result.GetDescription(), () =>
{ {
AddStep("Show " + result.GetDescription(), () => int poolIndex = 0;
SetContents(() =>
{ {
int poolIndex = 0; DrawablePool<TestDrawableOsuJudgement> pool;
SetContents(() => if (poolIndex >= pools.Count)
pools.Add(pool = new DrawablePool<TestDrawableOsuJudgement>(1));
else
{ {
DrawablePool<DrawableOsuJudgement> pool; pool = pools[poolIndex];
if (poolIndex >= pools.Count) // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
pools.Add(pool = new DrawablePool<DrawableOsuJudgement>(1)); ((Container)pool.Parent).Clear(false);
else }
var container = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
pool = pools[poolIndex]; pool,
pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
// We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
((Container)pool.Parent).Clear(false);
}
var container = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
pool, j.Anchor = Anchor.Centre;
pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j => j.Origin = Anchor.Centre;
{ })
j.Anchor = Anchor.Centre; }
j.Origin = Anchor.Centre; };
})
}
};
poolIndex++; poolIndex++;
return container; return container;
});
}); });
} });
}
private class TestDrawableOsuJudgement : DrawableOsuJudgement
{
public new SkinnableSprite Lighting => base.Lighting;
public new Container JudgementBody => base.JudgementBody;
} }
} }
} }

View File

@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
const double time_slider = 1500; const double time_slider = 1500;
const double time_circle = 1510; const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero; Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80); Vector2 positionSlider = new Vector2(30);
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {

View File

@ -9,6 +9,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -131,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}; };
} }
public Texture GetTexture(string componentName) => null; public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public SampleChannel GetSample(ISampleInfo sampleInfo) => null; public SampleChannel GetSample(ISampleInfo sampleInfo) => null;

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void testSingle(float circleSize, bool auto = false) private void testSingle(float circleSize, bool auto = false)
{ {
var spinner = new Spinner { StartTime = Time.Current + 1000, EndTime = Time.Current + 4000 }; var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });

View File

@ -1,26 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Utils; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool Autoplay => true; protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ {
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
@ -129,18 +133,44 @@ namespace osu.Game.Rulesets.Osu.Tests
.ToList() .ToList()
}; };
[Test]
public void TestSpinnerNormalBonusRewinding()
{
addSeekStep(1000);
AddAssert("player score matching expected bonus score", () =>
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK;
});
addSeekStep(0);
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
}
[Test]
public void TestSpinnerCompleteBonusRewinding()
{
addSeekStep(2500);
addSeekStep(0);
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
}
[Test] [Test]
public void TestSpinPerMinuteOnRewind() public void TestSpinPerMinuteOnRewind()
{ {
double estimatedSpm = 0; double estimatedSpm = 0;
addSeekStep(2500); addSeekStep(1000);
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
addSeekStep(5000); addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
addSeekStep(2500); addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
} }
@ -160,12 +190,17 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
EndTime = 6000, EndTime = 6000,
}, },
// placeholder object to avoid hitting the results screen
new HitCircle
{
StartTime = 99999,
}
} }
}; };
private class ScoreExposedPlayer : TestPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public ScoreExposedPlayer()
: base(false, false)
{
}
}
} }
} }

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager; private OsuInputManager inputManager;
private GameplayClock gameplayClock;
private List<OsuReplayFrame> replayFrames; private List<OsuReplayFrame> replayFrames;
private int currentFrame; private int currentFrame;
@ -38,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
if (currentFrame == replayFrames.Count - 1) return; if (currentFrame == replayFrames.Count - 1) return;
double time = playfield.Time.Current; double time = gameplayClock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames. // Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered). // TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
@ -53,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
gameplayClock = drawableRuleset.FrameStableClock;
// Grab the input manager to disable the user's cursor, and for future use // Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false; inputManager.AllowUserCursorMovement = false;

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Hit them at the right size!"; public override string Description => "Hit them at the right size!";
protected override float StartScale => 2f; [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat
{
MinValue = 1f,
MaxValue = 25f,
Default = 2f,
Value = 2f,
Precision = 0.1f,
};
} }
} }

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public BindableNumber<float> CircleSize { get; } = new BindableFloat public BindableNumber<float> CircleSize { get; } = new BindableFloat
{ {
Precision = 0.1f, Precision = 0.1f,
MinValue = 1, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Default = 5, Default = 5,
Value = 5, Value = 5,
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public BindableNumber<float> ApproachRate { get; } = new BindableFloat public BindableNumber<float> ApproachRate { get; } = new BindableFloat
{ {
Precision = 0.1f, Precision = 0.1f,
MinValue = 1, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Default = 5, Default = 5,
Value = 5, Value = 5,

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Hit them at the right size!"; public override string Description => "Hit them at the right size!";
protected override float StartScale => 0.5f; [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat
{
MinValue = 0f,
MaxValue = 0.99f,
Default = 0.5f,
Value = 0.5f,
Precision = 0.01f,
};
} }
} }

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
protected virtual float StartScale => 1; public abstract BindableNumber<float> StartScale { get; }
protected virtual float EndScale => 1; protected virtual float EndScale => 1;
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableHitCircle _: case DrawableHitCircle _:
{ {
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
drawable.ScaleTo(StartScale).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
break; break;
} }
} }

View File

@ -16,9 +16,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableOsuJudgement : DrawableJudgement public class DrawableOsuJudgement : DrawableJudgement
{ {
private SkinnableSprite lighting; protected SkinnableSprite Lighting;
private Bindable<Color4> lightingColour; private Bindable<Color4> lightingColour;
[Resolved]
private OsuConfigManager config { get; set; }
public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject) : base(result, judgedObject)
{ {
@ -29,18 +33,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load()
{ {
if (config.Get<bool>(OsuSetting.HitLighting)) AddInternal(Lighting = new SkinnableSprite("lighting")
{ {
AddInternal(lighting = new SkinnableSprite("lighting") Anchor = Anchor.Centre,
{ Origin = Anchor.Centre,
Anchor = Anchor.Centre, Blending = BlendingParameters.Additive,
Origin = Anchor.Centre, Depth = float.MaxValue,
Blending = BlendingParameters.Additive, Alpha = 0
Depth = float.MaxValue });
});
}
} }
public override void Apply(JudgementResult result, DrawableHitObject judgedObject) public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
@ -60,33 +62,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
lightingColour?.UnbindAll(); lightingColour?.UnbindAll();
if (lighting != null) Lighting.ResetAnimation();
{
lighting.ResetAnimation();
if (JudgedObject != null) if (JudgedObject != null)
{ {
lightingColour = JudgedObject.AccentColour.GetBoundCopy(); lightingColour = JudgedObject.AccentColour.GetBoundCopy();
lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
} }
else else
{ {
lighting.Colour = Color4.White; Lighting.Colour = Color4.White;
}
} }
} }
protected override double FadeOutDelay => lighting == null ? base.FadeOutDelay : 1400; private double fadeOutDelay;
protected override double FadeOutDelay => fadeOutDelay;
protected override void ApplyHitAnimations() protected override void ApplyHitAnimations()
{ {
if (lighting != null) bool hitLightingEnabled = config.Get<bool>(OsuSetting.HitLighting);
if (hitLightingEnabled)
{ {
JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400); JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
} }
else
{
JudgementBody.Alpha = 1;
}
fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations(); base.ApplyHitAnimations();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osuTK; using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -11,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -81,6 +83,42 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var drawableHitObject in NestedHitObjects) foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue; drawableHitObject.AccentColour.Value = colour.NewValue;
}, true); }, true);
Tracking.BindValueChanged(updateSlidingSample);
}
private SkinnableSound slidingSample;
protected override void LoadSamples()
{
base.LoadSamples();
slidingSample?.Expire();
slidingSample = null;
var firstSample = HitObject.Samples.FirstOrDefault();
if (firstSample != null)
{
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide";
AddInternal(slidingSample = new SkinnableSound(clone)
{
Looping = true
});
}
}
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
// note that samples will not start playing if exiting a seek operation in the middle of a slider.
// may be something we want to address at a later point, but not so easy to make happen right now
// (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
if (tracking.NewValue && ShouldPlaySamples)
slidingSample?.Play();
else
slidingSample?.Stop();
} }
protected override void AddNestedHitObject(DrawableHitObject hitObject) protected override void AddNestedHitObject(DrawableHitObject hitObject)
@ -156,6 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.Value = Ball.Tracking; Tracking.Value = Ball.Tracking;
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X);
double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
Ball.UpdateProgress(completionProgress); Ball.UpdateProgress(completionProgress);

View File

@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer; private readonly Drawable scaleContainer;
public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
: base(sliderRepeat) : base(sliderRepeat)
{ {

View File

@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -24,9 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
protected readonly Spinner Spinner; protected readonly Spinner Spinner;
private readonly Container<DrawableSpinnerTick> ticks;
public readonly SpinnerDisc Disc; public readonly SpinnerDisc Disc;
public readonly SpinnerTicks Ticks; public readonly SpinnerTicks Ticks;
public readonly SpinnerSpmCounter SpmCounter; public readonly SpinnerSpmCounter SpmCounter;
private readonly SpinnerBonusDisplay bonusDisplay;
private readonly Container mainContainer; private readonly Container mainContainer;
@ -60,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
ticks = new Container<DrawableSpinnerTick>(),
circleContainer = new Container circleContainer = new Container
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -93,7 +98,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
Background = new SpinnerBackground Background = new SpinnerBackground
{ {
Alpha = 0.6f, Disc =
{
Alpha = 0f,
},
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
@ -117,18 +125,56 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre, Origin = Anchor.Centre,
Y = 120, Y = 120,
Alpha = 0 Alpha = 0
},
bonusDisplay = new SpinnerBonusDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -120,
} }
}; };
} }
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
switch (hitObject)
{
case DrawableSpinnerTick tick:
ticks.Add(tick);
break;
}
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
ticks.Clear();
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case SpinnerBonusTick bonusTick:
return new DrawableSpinnerBonusTick(bonusTick);
case SpinnerTick tick:
return new DrawableSpinnerTick(tick);
}
return base.CreateNestedHitObject(hitObject);
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
normalColour = baseColour; normalColour = baseColour;
completeColour = colours.YellowLight;
Background.AccentColour = normalColour; Background.AccentColour = normalColour;
Ticks.AccentColour = normalColour;
completeColour = colours.YellowLight.Opacity(0.75f);
Disc.AccentColour = fillColour; Disc.AccentColour = fillColour;
circle.Colour = colours.BlueDark; circle.Colour = colours.BlueDark;
@ -147,21 +193,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Progress >= 1 && !Disc.Complete) if (Progress >= 1 && !Disc.Complete)
{ {
Disc.Complete = true; Disc.Complete = true;
transformFillColour(completeColour, 200);
const float duration = 200;
Disc.FadeAccent(completeColour, duration);
Background.FadeAccent(completeColour, duration);
Background.FadeOut(duration);
circle.FadeColour(completeColour, duration);
glow.FadeColour(completeColour, duration);
} }
if (userTriggered || Time.Current < Spinner.EndTime) if (userTriggered || Time.Current < Spinner.EndTime)
return; return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
foreach (var tick in ticks.Where(t => !t.IsHit))
tick.TriggerResult(false);
ApplyResult(r => ApplyResult(r =>
{ {
if (Progress >= 1) if (Progress >= 1)
@ -191,8 +232,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
circle.Rotation = Disc.Rotation; circle.Rotation = Disc.Rotation;
Ticks.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation;
SpmCounter.SetRotation(Disc.CumulativeRotation); SpmCounter.SetRotation(Disc.CumulativeRotation);
updateBonusScore();
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
@ -200,36 +244,95 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
} }
private int wholeSpins;
private void updateBonusScore()
{
if (ticks.Count == 0)
return;
int spins = (int)(Disc.CumulativeRotation / 360);
if (spins < wholeSpins)
{
// rewinding, silently handle
wholeSpins = spins;
return;
}
while (wholeSpins != spins)
{
var tick = ticks.FirstOrDefault(t => !t.IsHit);
// tick may be null if we've hit the spin limit.
if (tick != null)
{
tick.TriggerResult(true);
if (tick is DrawableSpinnerBonusTick)
bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired);
}
wholeSpins++;
}
}
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();
circleContainer.ScaleTo(Spinner.Scale * 0.3f); circleContainer.ScaleTo(0);
circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint); mainContainer.ScaleTo(0);
mainContainer using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
.ScaleTo(0) {
.ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint) float phaseOneScale = Spinner.Scale * 0.7f;
.Then()
.ScaleTo(1, 500, Easing.OutQuint); circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint);
mainContainer
.ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint)
.RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration);
using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
{
circleContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint);
mainContainer.ScaleTo(1, 400, Easing.OutQuint);
}
}
} }
protected override void UpdateStateTransforms(ArmedState state) protected override void UpdateStateTransforms(ArmedState state)
{ {
base.UpdateStateTransforms(state); base.UpdateStateTransforms(state);
var sequence = this.Delay(Spinner.Duration).FadeOut(160); using (BeginDelayedSequence(Spinner.Duration, true))
switch (state)
{ {
case ArmedState.Hit: this.FadeOut(160);
sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out);
break;
case ArmedState.Miss: switch (state)
sequence.ScaleTo(Scale * 0.8f, 320, Easing.In); {
break; case ArmedState.Hit:
transformFillColour(completeColour, 0);
this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
mainContainer.RotateTo(mainContainer.Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(Scale * 0.8f, 320, Easing.In);
break;
}
} }
} }
private void transformFillColour(Colour4 colour, double duration)
{
Disc.FadeAccent(colour, duration);
Background.FadeAccent(colour.Darken(1), duration);
Ticks.FadeAccent(colour, duration);
circle.FadeColour(colour, duration);
glow.FadeColour(colour, duration);
}
} }
} }

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerBonusTick : DrawableSpinnerTick
{
public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick)
: base(spinnerTick)
{
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerTick : DrawableOsuHitObject
{
public override bool DisplayResult => false;
public DrawableSpinnerTick(SpinnerTick spinnerTick)
: base(spinnerTick)
{
}
/// <summary>
/// Apply a judgement result.
/// </summary>
/// <param name="hit">Whether this tick was reached.</param>
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss);
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider; private readonly Slider slider;
private readonly Drawable followCircle; private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider; private readonly DrawableSlider drawableSlider;
private readonly CircularContainer ball; private readonly Drawable ball;
public SliderBall(Slider slider, DrawableSlider drawableSlider = null) public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{ {
@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Alpha = 0, Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
}, },
ball = new CircularContainer ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{ {
Masking = true,
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Alpha = 1, Origin = Anchor.Centre,
Child = new Container },
{
RelativeSizeAxes = Axes.Both,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()),
}
}
}; };
} }
@ -187,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return; return;
Position = newPos; Position = newPos;
Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = newPos; lastPosition = newPos;
} }
private class FollowCircleContainer : Container private class FollowCircleContainer : CircularContainer
{ {
public override bool HandlePositionalInput => true; public override bool HandlePositionalInput => true;
} }

View File

@ -1,18 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osuTK.Graphics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerBackground : CircularContainer, IHasAccentColour public class SpinnerBackground : CircularContainer, IHasAccentColour
{ {
protected Box Disc; public readonly Box Disc;
public Color4 AccentColour public Color4 AccentColour
{ {

View File

@ -0,0 +1,44 @@
// 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.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
/// <summary>
/// Shows incremental bonus score achieved for a spinner.
/// </summary>
public class SpinnerBonusDisplay : CompositeDrawable
{
private readonly OsuSpriteText bonusCounter;
public SpinnerBonusDisplay()
{
AutoSizeAxes = Axes.Both;
InternalChild = bonusCounter = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 24),
Alpha = 0,
};
}
private int displayedCount;
public void SetBonusCount(int count)
{
if (displayedCount == count)
return;
displayedCount = count;
bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}";
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -9,10 +10,11 @@ using osu.Framework.Graphics.Effects;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerTicks : Container public class SpinnerTicks : Container, IHasAccentColour
{ {
public SpinnerTicks() public SpinnerTicks()
{ {
@ -20,28 +22,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
const float count = 18; const float count = 8;
for (float i = 0; i < count; i++) for (float i = 0; i < count; i++)
{ {
Add(new Container Add(new Container
{ {
Colour = Color4.Black,
Alpha = 0.4f, Alpha = 0.4f,
EdgeEffect = new EdgeEffectParameters Blending = BlendingParameters.Additive,
{
Type = EdgeEffectType.Glow,
Radius = 10,
Colour = Color4.Gray.Opacity(0.2f),
},
RelativePositionAxes = Axes.Both, RelativePositionAxes = Axes.Both,
Masking = true, Masking = true,
CornerRadius = 5, CornerRadius = 5,
Size = new Vector2(60, 10), Size = new Vector2(60, 10),
Origin = Anchor.Centre, Origin = Anchor.Centre,
Position = new Vector2( Position = new Vector2(
0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f, 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.83f,
0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.86f 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.83f
), ),
Rotation = -i / count * 360 + 90, Rotation = -i / count * 360 + 90,
Children = new[] Children = new[]
@ -54,5 +50,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}); });
} }
} }
public Color4 AccentColour
{
get => Colour;
set
{
Colour = value;
foreach (var c in Children.OfType<Container>())
{
c.EdgeEffect =
new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 20,
Colour = value.Opacity(0.8f),
};
}
}
}
} }
} }

View File

@ -3,9 +3,9 @@
using System; using System;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -26,14 +26,43 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public int SpinsRequired { get; protected set; } = 1; public int SpinsRequired { get; protected set; } = 1;
/// <summary>
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
/// </summary>
public int MaximumBonusSpins { get; protected set; } = 1;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5));
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); const double stable_matching_fudge = 0.6;
// close to 477rpm
const double maximum_rotations_per_second = 8;
double secondsDuration = Duration / 1000;
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond));
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
}
protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
int totalSpins = MaximumBonusSpins + SpinsRequired;
for (int i = 0; i < totalSpins; i++)
{
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime }
: new SpinnerBonusTick { StartTime = startTime });
}
} }
public override Judgement CreateJudgement() => new OsuJudgement(); public override Judgement CreateJudgement() => new OsuJudgement();

View File

@ -0,0 +1,28 @@
// 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.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerBonusTick : SpinnerTick
{
public new const int SCORE_PER_TICK = 50;
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
}
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
}
}
}

View File

@ -0,0 +1,27 @@
// 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.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
public const int SCORE_PER_TICK = 10;
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public class OsuSpinnerTickJudgement : OsuJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
}
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays
/// </summary> /// </summary>
protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2; protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2;
protected const float SPIN_RADIUS = 50; public const float SPIN_RADIUS = 50;
/// <summary> /// <summary>
/// The time in ms between each ReplayFrame. /// The time in ms between each ReplayFrame.

View File

@ -36,19 +36,10 @@ namespace osu.Game.Rulesets.Osu.Replays
} }
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
return new List<IInput> inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
{ inputs.Add(new ReplayState<OsuAction> { PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>() });
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(Position ?? Vector2.Zero)
},
new ReplayState<OsuAction>
{
PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>()
}
};
} }
} }
} }

View File

@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
private readonly Drawable animationContent; private readonly Drawable animationContent;
private Sprite layerNd;
private Sprite layerSpec;
public LegacySliderBall(Drawable animationContent) public LegacySliderBall(Drawable animationContent)
{ {
this.animationContent = animationContent; this.animationContent = animationContent;
@ -29,18 +32,37 @@ namespace osu.Game.Rulesets.Osu.Skinning
InternalChildren = new[] InternalChildren = new[]
{ {
new Sprite layerNd = new Sprite
{ {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-nd"), Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255), Colour = new Color4(5, 5, 5, 255),
}, },
animationContent, animationContent.With(d =>
new Sprite
{ {
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}),
layerSpec = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-spec"), Texture = skin.GetTexture("sliderb-spec"),
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
}, },
}; };
} }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
//undo rotation on layers which should not be rotated.
float appliedRotation = Parent.Rotation;
layerNd.Rotation = -appliedRotation;
layerSpec.Rotation = -appliedRotation;
}
} }
} }

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
case OsuSkinComponents.HitCircleText: case OsuSkinComponents.HitCircleText:
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0; var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
return !hasFont(font) return !hasFont(font)
? null ? null

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -36,6 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private TaikoScoreProcessor scoreProcessor; private TaikoScoreProcessor scoreProcessor;
private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>(); private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>();
private IEnumerable<DrawableTaikoMascot> animatedMascots =>
mascots.Where(mascot => mascot.ChildrenOfType<TextureAnimation>().All(animation => animation.FrameCount > 0));
private IEnumerable<TaikoPlayfield> playfields => this.ChildrenOfType<TaikoPlayfield>(); private IEnumerable<TaikoPlayfield> playfields => this.ChildrenOfType<TaikoPlayfield>();
[SetUp] [SetUp]
@ -72,11 +77,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss })); AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear)); AddAssert("skins with animations remain in clear state", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail));
AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear)); AddAssert("skins with animations change to clear", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
} }
[Test] [Test]
@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail); assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
} }
@ -186,10 +191,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState) private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
{ {
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", TaikoMascotAnimationState[] mascotStates = null;
() => applyNewResult(judgementResult));
AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState)); AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
() =>
{
applyNewResult(judgementResult);
// store the states as soon as possible, so that the delay between steps doesn't incorrectly fail the test
// due to not checking if the state changed quickly enough.
Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
});
AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState));
} }
private void applyNewResult(JudgementResult judgementResult) private void applyNewResult(JudgementResult judgementResult)
@ -211,6 +224,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
} }
private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state);
private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state); private bool animatedMascotsIn(TaikoMascotAnimationState state) => animatedMascots.Any(d => d.State.Value == state);
} }
} }

View File

@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("basic")] [TestCase("basic")]
[TestCase("slider-generating-drumroll")] [TestCase("slider-generating-drumroll")]
[TestCase("sample-to-type-conversions")] [TestCase("sample-to-type-conversions")]
[TestCase("slider-conversion-v6")]
[TestCase("slider-conversion-v14")]
public void Test(string name) => base.Test(name); public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
}; };
[Test] [Test]
public void TestSpinnerDoesNotFail() public void TestSpinnerDoesFail()
{ {
bool judged = false; bool judged = false;
AddStep("Setup judgements", () => AddStep("Setup judgements", () =>
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Player.ScoreProcessor.NewJudgement += b => judged = true; Player.ScoreProcessor.NewJudgement += b => judged = true;
}); });
AddUntilStep("swell judged", () => judged); AddUntilStep("swell judged", () => judged);
AddAssert("not failed", () => !Player.HasFailed); AddAssert("failed", () => Player.HasFailed);
} }
} }
} }

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Audio
{
/// <summary>
/// Stores samples for the input drum.
/// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
/// </summary>
public class DrumSampleContainer : LifetimeManagementContainer
{
private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
public DrumSampleContainer(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
IReadOnlyList<SampleControlPoint> samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
for (int i = 0; i < samplePoints.Count; i++)
{
var samplePoint = samplePoints[i];
var centre = samplePoint.GetSampleInfo();
var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP);
var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue;
mappings[samplePoint.Time] = new DrumSample
{
Centre = addSound(centre, lifetimeStart, lifetimeEnd),
Rim = addSound(rim, lifetimeStart, lifetimeEnd)
};
}
}
private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
{
var drawable = new SkinnableSound(hitSampleInfo)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
};
AddInternal(drawable);
return drawable;
}
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
public class DrumSample
{
public SkinnableSound Centre;
public SkinnableSound Rim;
}
}
}

View File

@ -1,52 +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 System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Audio
{
public class DrumSampleMapping
{
private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
public readonly List<SkinnableSound> Sounds = new List<SkinnableSound>();
public DrumSampleMapping(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
IEnumerable<SampleControlPoint> samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
foreach (var s in samplePoints)
{
var centre = s.GetSampleInfo();
var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP);
mappings[s.Time] = new DrumSample
{
Centre = addSound(centre),
Rim = addSound(rim)
};
}
}
private SkinnableSound addSound(HitSampleInfo hitSampleInfo)
{
var drawable = new SkinnableSound(hitSampleInfo);
Sounds.Add(drawable);
return drawable;
}
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
public class DrumSample
{
public SkinnableSound Centre;
public SkinnableSound Rim;
}
}
}

View File

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Taiko.Beatmaps namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
@ -82,37 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
case IHasDistance distanceData: case IHasDistance distanceData:
{ {
// Number of spans of the object - one for the initial length and for each repeat if (shouldConvertSliderToHits(obj, beatmap, distanceData, out var taikoDuration, out var tickSpacing))
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
double speedAdjustment = difficultyPoint.SpeedMultiplier;
double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the taiko hit object
double taikoDuration = distance / taikoVelocity;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
// osu-stable always uses the speed-adjusted beatlength to determine the velocities, but
// only uses it for tick rate if beatmap version < 8
if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
speedAdjustedBeatLength *= speedAdjustment;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{ {
List<IList<HitSampleInfo>> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples }); List<IList<HitSampleInfo>> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples });
@ -184,6 +155,52 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
} }
} }
private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out double taikoDuration, out double tickSpacing)
{
// DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS.
// Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable.
// Rounding cannot be used as an alternative since the error deltas have been observed to be between 1e-2 and 1e-6.
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
double beatLength;
#pragma warning disable 618
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
taikoDuration = distance / taikoVelocity * beatLength;
if (isForCurrentRuleset)
{
tickSpacing = 0;
return false;
}
double osuVelocity = taikoVelocity * (1000f / beatLength);
// osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
beatLength = timingPoint.BeatLength;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
return tickSpacing > 0
&& distance / osuVelocity * 1000 < 2 * beatLength;
}
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap(); protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
double addition = 1; double addition = 1;
// We get an extra addition if we are not a slider or spinner // We get an extra addition if we are not a slider or spinner
if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000)
{ {
if (hasColourChange(current)) if (hasColourChange(current))
addition += 0.75; addition += 0.75;

View File

@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{ {
public class TaikoDrumRollJudgement : TaikoJudgement public class TaikoDrumRollJudgement : TaikoJudgement
{ {
public override bool AffectsCombo => false;
protected override double HealthIncreaseFor(HitResult result) protected override double HealthIncreaseFor(HitResult result)
{ {
// Drum rolls can be ignored with no health penalty // Drum rolls can be ignored with no health penalty

View File

@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{ {
public class TaikoSwellJudgement : TaikoJudgement public class TaikoSwellJudgement : TaikoJudgement
{ {
public override bool AffectsCombo => false;
protected override double HealthIncreaseFor(HitResult result) protected override double HealthIncreaseFor(HitResult result)
{ {
switch (result) switch (result)

View File

@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Replays
protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any();
public override List<IInput> GetPendingInputs() => new List<IInput> { new ReplayState<TaikoAction> { PressedActions = CurrentFrame?.Actions ?? new List<TaikoAction>() } }; public override void CollectPendingInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<TaikoAction> { PressedActions = CurrentFrame?.Actions ?? new List<TaikoAction>() });
}
} }
} }

View File

@ -0,0 +1,379 @@
{
"Mappings": [{
"StartTime": 2000,
"Objects": [{
"StartTime": 2000,
"EndTime": 2000,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": true
},
{
"StartTime": 2173,
"EndTime": 2173,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
}
]
},
{
"StartTime": 4000,
"Objects": [{
"StartTime": 4000,
"EndTime": 4000,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 4173,
"EndTime": 4173,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
}
]
},
{
"StartTime": 6000,
"Objects": [{
"StartTime": 6000,
"EndTime": 6000,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 6271,
"EndTime": 6271,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 6542,
"EndTime": 6542,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
}
]
},
{
"StartTime": 8000,
"Objects": [{
"StartTime": 8000,
"EndTime": 8000,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8026,
"EndTime": 8026,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8053,
"EndTime": 8053,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8080,
"EndTime": 8080,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8107,
"EndTime": 8107,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8133,
"EndTime": 8133,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8160,
"EndTime": 8160,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8187,
"EndTime": 8187,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8214,
"EndTime": 8214,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8241,
"EndTime": 8241,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8267,
"EndTime": 8267,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8294,
"EndTime": 8294,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8321,
"EndTime": 8321,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8348,
"EndTime": 8348,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8374,
"EndTime": 8374,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8401,
"EndTime": 8401,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8428,
"EndTime": 8428,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8455,
"EndTime": 8455,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8482,
"EndTime": 8482,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8508,
"EndTime": 8508,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8535,
"EndTime": 8535,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8562,
"EndTime": 8562,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8589,
"EndTime": 8589,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8615,
"EndTime": 8615,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8642,
"EndTime": 8642,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8669,
"EndTime": 8669,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8696,
"EndTime": 8696,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8723,
"EndTime": 8723,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8749,
"EndTime": 8749,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8776,
"EndTime": 8776,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8803,
"EndTime": 8803,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8830,
"EndTime": 8830,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8857,
"EndTime": 8857,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
}
]
}
]
}

View File

@ -0,0 +1,32 @@
osu file format v14
[General]
Mode: 0
[Difficulty]
HPDrainRate:7
CircleSize:4
OverallDifficulty:8
ApproachRate:9.2
SliderMultiplier:2.3
SliderTickRate:1
[TimingPoints]
0,333.333333333333,4,1,0,50,1,0
2000,-100,4,2,0,80,0,0
6000,389.61038961039,4,2,1,60,1,0
8000,428.571428571429,4,3,1,65,1,0
8000,-133.333333333333,4,1,1,45,0,0
[HitObjects]
// Should convert.
48,32,2000,6,0,B|168:32,1,120,4|2
312,68,4000,2,0,B|288:52|256:44|216:52|200:68,1,120,0|8
// Should convert.
184,224,6000,2,0,L|336:308,2,160,2|2|0,0:0|0:0|0:0,0:0:0:0:
// Should convert.
328,36,8000,6,0,L|332:16,32,10.7812504112721,0|0,0:0,0:0:0:0:

View File

@ -0,0 +1,137 @@
{
"Mappings": [{
"StartTime": 0,
"Objects": [{
"StartTime": 0,
"EndTime": 0,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": true
},
{
"StartTime": 162,
"EndTime": 162,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 325,
"EndTime": 325,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": true
},
{
"StartTime": 487,
"EndTime": 487,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 650,
"EndTime": 650,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": true
},
{
"StartTime": 813,
"EndTime": 813,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 975,
"EndTime": 975,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": true
}
]
},
{
"StartTime": 2000,
"Objects": [{
"StartTime": 2000,
"EndTime": 2000,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 2162,
"EndTime": 2162,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 2325,
"EndTime": 2325,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": true
},
{
"StartTime": 2487,
"EndTime": 2487,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 2650,
"EndTime": 2650,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 2813,
"EndTime": 2813,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": true
},
{
"StartTime": 2975,
"EndTime": 2975,
"IsRim": true,
"IsCentre": false,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
}
]
}
]
}

View File

@ -0,0 +1,20 @@
osu file format v6
[General]
Mode: 0
[Difficulty]
HPDrainRate:3
CircleSize:4
OverallDifficulty:1
SliderMultiplier:1.2
SliderTickRate:3
[TimingPoints]
0,487.884208814441,4,1,0,60,1,0
2000,-100,4,1,0,65,0,1
[HitObjects]
// Should convert.
376,64,0,6,0,B|256:32|136:64,1,240,6|0
256,120,2000,6,8,C|264:192|336:192,2,120,8|8|6

View File

@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{ {
internal class TaikoScoreProcessor : ScoreProcessor internal class TaikoScoreProcessor : ScoreProcessor
{ {
protected override double DefaultAccuracyPortion => 0.75;
protected override double DefaultComboPortion => 0.25;
public override HitWindows CreateHitWindows() => new TaikoHitWindows(); public override HitWindows CreateHitWindows() => new TaikoHitWindows();
} }
} }

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -34,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-end"), Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
}, },
body = new Sprite body = new Sprite
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-middle"), Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
}, },
headCircle = new LegacyCirclePiece headCircle = new LegacyCirclePiece
{ {

View File

@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public readonly Sprite Centre; public readonly Sprite Centre;
[Resolved] [Resolved]
private DrumSampleMapping sampleMappings { get; set; } private DrumSampleContainer sampleContainer { get; set; }
public LegacyHalfDrum(bool flipped) public LegacyHalfDrum(bool flipped)
{ {
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public bool OnPressed(TaikoAction action) public bool OnPressed(TaikoAction action)
{ {
Drawable target = null; Drawable target = null;
var drumSample = sampleMappings.SampleAt(Time.Current); var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction) if (action == CentreAction)
{ {

View File

@ -91,10 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null; return null;
case TaikoSkinComponents.Mascot: case TaikoSkinComponents.Mascot:
if (GetTexture("pippidonclear0") != null) return new DrawableTaikoMascot();
return new DrawableTaikoMascot();
return null;
} }
return Source.GetDrawableComponent(component); return Source.GetDrawableComponent(component);

View File

@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f; private const float middle_split = 0.025f;
[Cached] [Cached]
private DrumSampleMapping sampleMapping; private DrumSampleContainer sampleContainer;
public InputDrum(ControlPointInfo controlPoints) public InputDrum(ControlPointInfo controlPoints)
{ {
sampleMapping = new DrumSampleMapping(controlPoints); sampleContainer = new DrumSampleContainer(controlPoints);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -37,39 +37,41 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
FillMode = FillMode.Fit,
Scale = new Vector2(0.9f),
Children = new Drawable[]
{ {
new TaikoHalfDrum(false) RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Scale = new Vector2(0.9f),
Children = new Drawable[]
{ {
Name = "Left Half", new TaikoHalfDrum(false)
Anchor = Anchor.Centre, {
Origin = Anchor.CentreRight, Name = "Left Half",
RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre,
RelativePositionAxes = Axes.X, Origin = Anchor.CentreRight,
X = -middle_split / 2, RelativeSizeAxes = Axes.Both,
RimAction = TaikoAction.LeftRim, RelativePositionAxes = Axes.X,
CentreAction = TaikoAction.LeftCentre X = -middle_split / 2,
}, RimAction = TaikoAction.LeftRim,
new TaikoHalfDrum(true) CentreAction = TaikoAction.LeftCentre
{ },
Name = "Right Half", new TaikoHalfDrum(true)
Anchor = Anchor.Centre, {
Origin = Anchor.CentreLeft, Name = "Right Half",
RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre,
RelativePositionAxes = Axes.X, Origin = Anchor.CentreLeft,
X = middle_split / 2, RelativeSizeAxes = Axes.Both,
RimAction = TaikoAction.RightRim, RelativePositionAxes = Axes.X,
CentreAction = TaikoAction.RightCentre X = middle_split / 2,
RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre
}
} }
} }),
}); sampleContainer
};
AddRangeInternal(sampleMapping.Sounds);
} }
/// <summary> /// <summary>
@ -93,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit; private readonly Sprite centreHit;
[Resolved] [Resolved]
private DrumSampleMapping sampleMappings { get; set; } private DrumSampleContainer sampleContainer { get; set; }
public TaikoHalfDrum(bool flipped) public TaikoHalfDrum(bool flipped)
{ {
@ -154,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null; Drawable target = null;
Drawable back = null; Drawable back = null;
var drumSample = sampleMappings.SampleAt(Time.Current); var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction) if (action == CentreAction)
{ {

View File

@ -128,6 +128,13 @@ namespace osu.Game.Rulesets.Taiko.UI
} }
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex) private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
=> skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); {
var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
if (frameIndex == 0 && texture == null)
texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}");
return texture;
}
} }
} }

View File

@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
@ -118,7 +119,7 @@ namespace osu.Game.Tests.Gameplay
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
public Texture GetTexture(string componentName) => throw new NotImplementedException(); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();

View File

@ -8,7 +8,6 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osu.Game.Users;
namespace osu.Game.Tests.Online namespace osu.Game.Tests.Online
{ {
@ -55,7 +54,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () => AddStep("fire request", () =>
{ {
gotResponse = false; gotResponse = false;
request = new LeaveChannelRequest(new Channel(), new User()); request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true; request.Success += () => gotResponse = true;
API.Queue(request); API.Queue(request);
}); });
@ -74,7 +73,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () => AddStep("fire request", () =>
{ {
gotResponse = false; gotResponse = false;
request = new LeaveChannelRequest(new Channel(), new User()); request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true; request.Success += () => gotResponse = true;
API.Perform(request); API.Perform(request);
}); });
@ -93,7 +92,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () => AddStep("fire request", () =>
{ {
gotResponse = false; gotResponse = false;
request = new LeaveChannelRequest(new Channel(), new User()); request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true; request.Success += () => gotResponse = true;
API.PerformAsync(request); API.PerformAsync(request);
}); });

View File

@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
@ -216,7 +217,7 @@ namespace osu.Game.Tests.Skins
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component); public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
public Texture GetTexture(string componentName) => skin.GetTexture(componentName); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); public SampleChannel GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);

View File

@ -175,13 +175,13 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Time = 50", () => Clock.CurrentTime == 50); AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999)); AddStep("Seek(49.999)", () => Clock.Seek(49.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(99)", () => Clock.Seek(99)); AddStep("Seek(99)", () => Clock.Seek(99));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(99.999)", () => Clock.Seek(99.999)); AddStep("Seek(99.999)", () => Clock.Seek(99.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); AddAssert("Time = 100", () => Clock.CurrentTime == 150);
AddStep("Seek(174)", () => Clock.Seek(174)); AddStep("Seek(174)", () => Clock.Seek(174));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); AddAssert("Time = 175", () => Clock.CurrentTime == 175);

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
@ -13,6 +14,7 @@ namespace osu.Game.Tests.Visual.Editing
public class TestSceneTimingScreen : EditorClockTestScene public class TestSceneTimingScreen : EditorClockTestScene
{ {
[Cached(typeof(EditorBeatmap))] [Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
public TestSceneTimingScreen() public TestSceneTimingScreen()

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -35,6 +36,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
double? time = null;
AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
// test seek via keyboard
AddStep("seek with right arrow key", () => press(Key.Right));
AddAssert("time seeked forward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime > time + 2000);
AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
AddStep("seek with left arrow key", () => press(Key.Left));
AddAssert("time seeked backward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < time);
seekToBreak(0); seekToBreak(0);
seekToBreak(1); seekToBreak(1);
@ -54,5 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex); BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex);
} }
private void press(Key key)
{
InputManager.PressKey(key);
InputManager.ReleaseKey(key);
}
} }
} }

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -221,6 +222,31 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmExited(); confirmExited();
} }
[Test]
public void TestPauseSoundLoop()
{
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
SkinnableSound getLoop() => Player.ChildrenOfType<PauseOverlay>().FirstOrDefault()?.ChildrenOfType<SkinnableSound>().FirstOrDefault();
pauseAndConfirm();
AddAssert("loop is playing", () => getLoop().IsPlaying);
resumeAndConfirm();
AddUntilStep("loop is stopped", () => !getLoop().IsPlaying);
AddUntilStep("pause again", () =>
{
Player.Pause();
return !Player.GameplayClockContainer.GameplayClock.IsRunning;
});
AddAssert("loop is playing", () => getLoop().IsPlaying);
resumeAndConfirm();
AddUntilStep("loop is stopped", () => !getLoop().IsPlaying);
}
private void pauseAndConfirm() private void pauseAndConfirm()
{ {
pause(); pause();

View File

@ -173,19 +173,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
return new List<IInput> inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
{ inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
},
new ReplayState<TestAction>
{
PressedActions = CurrentFrame?.Actions ?? new List<TestAction>()
}
};
} }
} }

View File

@ -113,19 +113,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
return new List<IInput> inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
{ inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
},
new ReplayState<TestAction>
{
PressedActions = CurrentFrame?.Actions ?? new List<TestAction>()
}
};
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
@ -295,7 +296,7 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
: null; : null;
public Texture GetTexture(string componentName) => throw new NotImplementedException(); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
@ -306,7 +307,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public Drawable GetDrawableComponent(ISkinComponent componentName) => new SecondarySourceBox(); public Drawable GetDrawableComponent(ISkinComponent componentName) => new SecondarySourceBox();
public Texture GetTexture(string componentName) => throw new NotImplementedException(); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public Drawable GetDrawableComponent(ISkinComponent componentName) => new BaseSourceBox(); public Drawable GetDrawableComponent(ISkinComponent componentName) => new BaseSourceBox();
public Texture GetTexture(string componentName) => throw new NotImplementedException(); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableSound : OsuTestScene
{
[Cached]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private SkinnableSound skinnableSound;
[SetUp]
public void SetUp() => Schedule(() =>
{
gameplayClock.IsPaused.Value = false;
Children = new Drawable[]
{
new Container
{
Clock = gameplayClock,
RelativeSizeAxes = Axes.Both,
Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide"))
},
};
});
[Test]
public void TestStoppedSoundDoesntResumeAfterPause()
{
DrawableSample sample = null;
AddStep("start sample with looping", () =>
{
sample = skinnableSound.ChildrenOfType<DrawableSample>().First();
skinnableSound.Looping = true;
skinnableSound.Play();
});
AddUntilStep("wait for sample to start playing", () => sample.Playing);
AddStep("stop sample", () => skinnableSound.Stop());
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
AddWaitStep("wait a bit", 5);
AddAssert("sample not playing", () => !sample.Playing);
}
[Test]
public void TestLoopingSoundResumesAfterPause()
{
DrawableSample sample = null;
AddStep("start sample with looping", () =>
{
skinnableSound.Looping = true;
skinnableSound.Play();
sample = skinnableSound.ChildrenOfType<DrawableSample>().First();
});
AddUntilStep("wait for sample to start playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
}
[Test]
public void TestNonLoopingStopsWithPause()
{
DrawableSample sample = null;
AddStep("start sample", () =>
{
skinnableSound.Play();
sample = skinnableSound.ChildrenOfType<DrawableSample>().First();
});
AddAssert("sample playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
AddAssert("sample not playing", () => !sample.Playing);
AddAssert("sample not playing", () => !sample.Playing);
AddAssert("sample not playing", () => !sample.Playing);
}
}
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -65,11 +64,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void bindHandler(double delay = 0) private void bindHandler(double delay = 0)
{ {
var roomScores = new List<RoomScore>(); var roomScores = new List<MultiplayerScore>();
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
roomScores.Add(new RoomScore roomScores.Add(new MultiplayerScore
{ {
ID = i, ID = i,
Accuracy = 0.9 - 0.01 * i, Accuracy = 0.9 - 0.01 * i,

View File

@ -30,12 +30,6 @@ namespace osu.Game.Tests.Visual.Online
Add(selector = new SpotlightSelector()); Add(selector = new SpotlightSelector());
} }
[Test]
public void TestVisibility()
{
AddStep("Toggle Visibility", selector.ToggleVisibility);
}
[Test] [Test]
public void TestLocalSpotlights() public void TestLocalSpotlights()
{ {

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Rankings;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
onLoadStarted(); onLoadStarted();
request = new GetSpotlightRankingsRequest(ruleset, spotlight); request = new GetSpotlightRankingsRequest(ruleset, spotlight, RankingsSortCriteria.All);
((GetSpotlightRankingsRequest)request).Success += rankings => Schedule(() => ((GetSpotlightRankingsRequest)request).Success += rankings => Schedule(() =>
{ {
var table = new ScoresTable(1, rankings.Users); var table = new ScoresTable(1, rankings.Users);

View File

@ -1,84 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Overlays;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneSocialOverlay : OsuTestScene
{
protected override bool UseOnlineAPI => true;
public TestSceneSocialOverlay()
{
SocialOverlay s = new SocialOverlay
{
Users = new[]
{
new User
{
Username = @"flyte",
Id = 3103765,
Country = new Country { FlagName = @"JP" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
},
new User
{
Username = @"Cookiezi",
Id = 124493,
Country = new Country { FlagName = @"KR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
},
new User
{
Username = @"Angelsim",
Id = 1777162,
Country = new Country { FlagName = @"KR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
new User
{
Username = @"Rafis",
Id = 2558286,
Country = new Country { FlagName = @"PL" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg",
},
new User
{
Username = @"hvick225",
Id = 50265,
Country = new Country { FlagName = @"TW" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg",
},
new User
{
Username = @"peppy",
Id = 2,
Country = new Country { FlagName = @"AU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
},
new User
{
Username = @"filsdelama",
Id = 2831793,
Country = new Country { FlagName = @"FR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg"
},
new User
{
Username = @"_index",
Id = 652457,
Country = new Country { FlagName = @"RU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg"
},
},
};
Add(s);
AddStep(@"toggle", s.ToggleVisibility);
}
}
}

View File

@ -42,6 +42,19 @@ namespace osu.Game.Tests.Visual.Online
Spacing = new Vector2(10f), Spacing = new Vector2(10f),
Children = new Drawable[] Children = new Drawable[]
{ {
new UserBrickPanel(new User
{
Username = @"flyte",
Id = 3103765,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
}),
new UserBrickPanel(new User
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}),
flyte = new UserGridPanel(new User flyte = new UserGridPanel(new User
{ {
Username = @"flyte", Username = @"flyte",

View File

@ -0,0 +1,67 @@
// 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.Overlays.Comments.Buttons;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Game.Overlays;
using osu.Framework.Graphics.Containers;
using osuTK;
using NUnit.Framework;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneCommentRepliesButton : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private readonly TestButton button;
public TestSceneCommentRepliesButton()
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
button = new TestButton(),
new LoadRepliesButton
{
Action = () => { }
},
new ShowRepliesButton(1),
new ShowRepliesButton(2)
}
};
}
[Test]
public void TestArrowDirection()
{
AddStep("Set upwards", () => button.SetIconDirection(true));
AddAssert("Icon facing upwards", () => button.Icon.Scale.Y == -1);
AddStep("Set downwards", () => button.SetIconDirection(false));
AddAssert("Icon facing downwards", () => button.Icon.Scale.Y == 1);
}
private class TestButton : CommentRepliesButton
{
public SpriteIcon Icon => this.ChildrenOfType<SpriteIcon>().First();
public TestButton()
{
Text = "sample text";
}
public new void SetIconDirection(bool upwards) => base.SetIconDirection(upwards);
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneLogoAnimation : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
LogoAnimation anim2;
Add(anim2 = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = textures.Get("Intro/Triangles/logo-highlight"),
Colour = Colour4.White,
});
LogoAnimation anim;
Add(anim = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = textures.Get("Intro/Triangles/logo-background"),
Colour = OsuColour.Gray(0.6f),
});
AddSliderStep("Progress", 0f, 1f, 0f, newValue =>
{
anim2.AnimationProgress = newValue;
anim.AnimationProgress = newValue;
});
}
}
}

View File

@ -13,6 +13,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -99,6 +101,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestManiaMods() public void TestManiaMods()
{ {
changeRuleset(3); changeRuleset(3);
var mania = new ManiaRuleset();
testModsWithSameBaseType(
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)),
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden)));
} }
[Test] [Test]
@ -197,6 +205,18 @@ namespace osu.Game.Tests.Visual.UserInterface
checkLabelColor(() => Color4.White); checkLabelColor(() => Color4.White);
} }
private void testModsWithSameBaseType(Mod modA, Mod modB)
{
selectNext(modA);
checkSelected(modA);
selectNext(modB);
checkSelected(modB);
// Backwards
selectPrevious(modA);
checkSelected(modA);
}
private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1)); private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1)); private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));

View File

@ -36,11 +36,11 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}); });
addHeader("Orange OverlayHeader (no background)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
addHeader("Blue OverlayHeader", new TestNoControlHeader(), OverlayColourScheme.Blue); addHeader("Blue OverlayHeader (default 50 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue);
addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green); addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green);
addHeader("Pink TabControlOverlayHeader (enum)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
addHeader("Red BreadcrumbControlOverlayHeader (no background)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
} }
private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme) private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme)
@ -86,6 +86,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestNoBackgroundHeader : OverlayHeader private class TestNoBackgroundHeader : OverlayHeader
{ {
protected override OverlayTitle CreateTitle() => new TestTitle(); protected override OverlayTitle CreateTitle() => new TestTitle();
public TestNoBackgroundHeader()
{
ContentSidePadding = 100;
}
} }
private class TestNoControlHeader : OverlayHeader private class TestNoControlHeader : OverlayHeader
@ -112,6 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestEnumTabControlHeader : TabControlOverlayHeader<TestEnum> private class TestEnumTabControlHeader : TabControlOverlayHeader<TestEnum>
{ {
public TestEnumTabControlHeader()
{
ContentSidePadding = 30;
}
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings"); protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
protected override OverlayTitle CreateTitle() => new TestTitle(); protected override OverlayTitle CreateTitle() => new TestTitle();
@ -130,6 +140,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestBreadcrumbControlHeader() public TestBreadcrumbControlHeader()
{ {
ContentSidePadding = 10;
TabControl.AddItem("tab1"); TabControl.AddItem("tab1");
TabControl.AddItem("tab2"); TabControl.AddItem("tab2");
TabControl.Current.Value = "tab2"; TabControl.Current.Value = "tab2";

View File

@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = convertHitObjects(original.HitObjects, original); beatmap.HitObjects = convertHitObjects(original.HitObjects, original).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks; beatmap.Breaks = original.Breaks;
return beatmap; return beatmap;

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