mirror of
https://github.com/ppy/osu.git
synced 2026-05-15 07:02:36 +08:00
Compare commits
308 Commits
2020.714.0
...
2020.725.0
+1
-1
@@ -5,6 +5,6 @@
|
||||
"version": "3.1.100"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "2.0.50"
|
||||
"Microsoft.Build.Traversal": "2.0.52"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.714.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.714.1" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.723.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -16,6 +16,7 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Updater;
|
||||
using osu.Desktop.Windows;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
@@ -98,6 +99,9 @@ namespace osu.Desktop
|
||||
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
|
||||
|
||||
LoadComponentAsync(new DiscordRichPresence(), Add);
|
||||
|
||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
|
||||
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
|
||||
}
|
||||
|
||||
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
@@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
private RulesetInfo catchRuleset;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
private Catcher catcher => this.ChildrenOfType<CatcherArea>().First().MovableCatcher;
|
||||
|
||||
public TestSceneCatcherArea()
|
||||
{
|
||||
AddSliderStep<float>("CircleSize", 0, 8, 5, createCatcher);
|
||||
@@ -34,24 +40,43 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
|
||||
{
|
||||
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X
|
||||
X = catcher.X
|
||||
}), 20);
|
||||
AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
|
||||
{
|
||||
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X,
|
||||
X = catcher.X,
|
||||
LastInCombo = true,
|
||||
}), 20);
|
||||
AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
|
||||
{
|
||||
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X,
|
||||
X = catcher.X
|
||||
}), 20);
|
||||
AddRepeatStep("miss fruit", () => catchFruit(new Fruit
|
||||
{
|
||||
X = this.ChildrenOfType<CatcherArea>().First().MovableCatcher.X + 100,
|
||||
X = catcher.X + 100,
|
||||
LastInCombo = true,
|
||||
}, 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)
|
||||
{
|
||||
this.ChildrenOfType<CatcherArea>().ForEach(area =>
|
||||
|
||||
@@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
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
|
||||
if (approachRate <= 10.0)
|
||||
value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModPerfect : ModPerfect
|
||||
{
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
=> !(result.Judgement is CatchBananaJudgement)
|
||||
&& base.FailCondition(healthProcessor, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -35,22 +35,25 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
|
||||
{
|
||||
Container explodingFruitContainer;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
var explodingFruitContainer = new Container
|
||||
{
|
||||
explodingFruitContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
CatcherArea = new CatcherArea(difficulty)
|
||||
{
|
||||
CreateDrawableRepresentation = createDrawableRepresentation,
|
||||
ExplodingFruitTarget = explodingFruitContainer,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
},
|
||||
HitObjectContainer
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
CatcherArea = new CatcherArea(difficulty)
|
||||
{
|
||||
CreateDrawableRepresentation = createDrawableRepresentation,
|
||||
ExplodingFruitTarget = explodingFruitContainer,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
};
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
explodingFruitContainer,
|
||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
||||
HitObjectContainer,
|
||||
CatcherArea
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
@@ -46,6 +48,12 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public Container ExplodingFruitTarget;
|
||||
|
||||
private Container<DrawableHitObject> caughtFruitContainer { get; } = new Container<DrawableHitObject>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
};
|
||||
|
||||
[NotNull]
|
||||
private readonly Container trailsTarget;
|
||||
|
||||
@@ -83,8 +91,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private readonly float catchWidth;
|
||||
|
||||
private Container<DrawableHitObject> caughtFruit;
|
||||
|
||||
private CatcherSprite catcherIdle;
|
||||
private CatcherSprite catcherKiai;
|
||||
private CatcherSprite catcherFail;
|
||||
@@ -99,6 +105,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
private double hyperDashModifier = 1;
|
||||
private int hyperDashDirection;
|
||||
private float hyperDashTargetPosition;
|
||||
private Bindable<bool> hitLighting;
|
||||
|
||||
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
@@ -114,15 +121,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
caughtFruit = new Container<DrawableHitObject>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
},
|
||||
caughtFruitContainer,
|
||||
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -145,6 +150,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
updateCatcher();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates proxied content to be displayed beneath hitobjects.
|
||||
/// </summary>
|
||||
public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy();
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
||||
/// </summary>
|
||||
@@ -176,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
const float allowance = 10;
|
||||
|
||||
while (caughtFruit.Any(f =>
|
||||
while (caughtFruitContainer.Any(f =>
|
||||
f.LifetimeEnd == double.MaxValue &&
|
||||
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);
|
||||
|
||||
caughtFruit.Add(fruit);
|
||||
caughtFruitContainer.Add(fruit);
|
||||
|
||||
AddInternal(new HitExplosion(fruit)
|
||||
if (hitLighting.Value)
|
||||
{
|
||||
X = fruit.X,
|
||||
Scale = new Vector2(fruit.HitObject.Scale)
|
||||
});
|
||||
AddInternal(new HitExplosion(fruit)
|
||||
{
|
||||
X = fruit.X,
|
||||
Scale = new Vector2(fruit.HitObject.Scale)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -342,7 +355,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
public void Drop()
|
||||
{
|
||||
foreach (var f in caughtFruit.ToArray())
|
||||
foreach (var f in caughtFruitContainer.ToArray())
|
||||
Drop(f);
|
||||
}
|
||||
|
||||
@@ -351,7 +364,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
public void Explode()
|
||||
{
|
||||
foreach (var f in caughtFruit.ToArray())
|
||||
foreach (var f in caughtFruitContainer.ToArray())
|
||||
Explode(f);
|
||||
}
|
||||
|
||||
@@ -450,9 +463,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (ExplodingFruitTarget != null)
|
||||
{
|
||||
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).
|
||||
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
|
||||
return;
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
|
||||
|
||||
public readonly Catcher MovableCatcher;
|
||||
|
||||
public Container ExplodingFruitTarget
|
||||
{
|
||||
set => MovableCatcher.ExplodingFruitTarget = value;
|
||||
@@ -104,7 +106,5 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (state?.CatcherX != null)
|
||||
MovableCatcher.X = state.CatcherX.Value;
|
||||
}
|
||||
|
||||
protected internal readonly Catcher MovableCatcher;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -10,6 +11,8 @@ using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
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.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@@ -236,6 +239,53 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
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)
|
||||
=> AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
|
||||
|
||||
@@ -250,11 +300,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
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 =
|
||||
{
|
||||
@@ -270,9 +320,14 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
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 } });
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModFadeIn : Mod
|
||||
public class ManiaModFadeIn : ManiaModHidden
|
||||
{
|
||||
public override string Name => "Fade In";
|
||||
public override string Acronym => "FI";
|
||||
public override IconUsage? Icon => OsuIcon.ModHidden;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) };
|
||||
|
||||
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,44 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
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.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
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 double ScoreMultiplier => 1;
|
||||
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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (action != Action.Value)
|
||||
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);
|
||||
Head.UpdateResult();
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
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>() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
internal class ManiaScoreProcessor : ScoreProcessor
|
||||
{
|
||||
protected override double DefaultAccuracyPortion => 0.8;
|
||||
|
||||
protected override double DefaultComboPortion => 0.2;
|
||||
|
||||
public override HitWindows CreateHitWindows() => new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
@@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
|
||||
?? $"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)
|
||||
return;
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
@@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
string noteImage = GetColumnSkinConfig<string>(skin, lookup)?.Value
|
||||
?? $"mania-note{FallbackColumnIndex}{suffix}";
|
||||
|
||||
return skin.GetTexture(noteImage);
|
||||
return skin.GetTexture(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
|
||||
private readonly ColumnHitObjectArea hitObjectArea;
|
||||
public readonly ColumnHitObjectArea HitObjectArea;
|
||||
|
||||
internal readonly Container TopLevelContainer;
|
||||
|
||||
public Container UnderlayElements => hitObjectArea.UnderlayElements;
|
||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||
|
||||
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
|
||||
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())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
|
||||
TopLevelContainer.Add(hitObjectArea.Explosions.CreateProxy());
|
||||
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
||||
}
|
||||
|
||||
public override Axes RelativeSizeAxes => Axes.Y;
|
||||
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
|
||||
hitObjectArea.Explosions.Add(explosion);
|
||||
HitObjectArea.Explosions.Add(explosion);
|
||||
|
||||
explosion.Delay(200).Expire(true);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
@@ -14,12 +15,14 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
public class HitObjectArea : SkinReloadableDrawable
|
||||
{
|
||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
public readonly HitObjectContainer HitObjectContainer;
|
||||
|
||||
public HitObjectArea(HitObjectContainer hitObjectContainer)
|
||||
{
|
||||
InternalChildren = new[]
|
||||
InternalChild = new Container
|
||||
{
|
||||
hitObjectContainer,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = HitObjectContainer = hitObjectContainer
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Audio;
|
||||
@@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
|
||||
|
||||
public Texture GetTexture(string componentName)
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
{
|
||||
switch (componentName)
|
||||
{
|
||||
|
||||
@@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
const double time_slider = 1500;
|
||||
const double time_circle = 1510;
|
||||
Vector2 positionCircle = Vector2.Zero;
|
||||
Vector2 positionSlider = new Vector2(80);
|
||||
Vector2 positionSlider = new Vector2(30);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
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;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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 });
|
||||
|
||||
|
||||
@@ -1,26 +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 System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
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.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
protected override bool Autoplay => true;
|
||||
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
{
|
||||
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
@@ -129,18 +133,44 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
.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) * 100;
|
||||
});
|
||||
|
||||
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]
|
||||
public void TestSpinPerMinuteOnRewind()
|
||||
{
|
||||
double estimatedSpm = 0;
|
||||
|
||||
addSeekStep(2500);
|
||||
addSeekStep(1000);
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
||||
|
||||
addSeekStep(5000);
|
||||
addSeekStep(2000);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -160,12 +190,17 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Position = new Vector2(256, 192),
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osuTK;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@@ -11,6 +12,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
@@ -81,6 +83,42 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
foreach (var drawableHitObject in NestedHitObjects)
|
||||
drawableHitObject.AccentColour.Value = colour.NewValue;
|
||||
}, 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)
|
||||
@@ -156,6 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
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);
|
||||
|
||||
Ball.UpdateProgress(completionProgress);
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private readonly Drawable scaleContainer;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
|
||||
: base(sliderRepeat)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
@@ -24,9 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
protected readonly Spinner Spinner;
|
||||
|
||||
private readonly Container<DrawableSpinnerTick> ticks;
|
||||
|
||||
public readonly SpinnerDisc Disc;
|
||||
public readonly SpinnerTicks Ticks;
|
||||
public readonly SpinnerSpmCounter SpmCounter;
|
||||
private readonly SpinnerBonusDisplay bonusDisplay;
|
||||
|
||||
private readonly Container mainContainer;
|
||||
|
||||
@@ -60,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
ticks = new Container<DrawableSpinnerTick>(),
|
||||
circleContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -93,7 +98,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
Background = new SpinnerBackground
|
||||
{
|
||||
Alpha = 0.6f,
|
||||
Disc =
|
||||
{
|
||||
Alpha = 0f,
|
||||
},
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
@@ -117,18 +125,56 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre,
|
||||
Y = 120,
|
||||
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]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
normalColour = baseColour;
|
||||
completeColour = colours.YellowLight;
|
||||
|
||||
Background.AccentColour = normalColour;
|
||||
|
||||
completeColour = colours.YellowLight.Opacity(0.75f);
|
||||
Ticks.AccentColour = normalColour;
|
||||
|
||||
Disc.AccentColour = fillColour;
|
||||
circle.Colour = colours.BlueDark;
|
||||
@@ -147,21 +193,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (Progress >= 1 && !Disc.Complete)
|
||||
{
|
||||
Disc.Complete = true;
|
||||
|
||||
const float duration = 200;
|
||||
|
||||
Disc.FadeAccent(completeColour, duration);
|
||||
|
||||
Background.FadeAccent(completeColour, duration);
|
||||
Background.FadeOut(duration);
|
||||
|
||||
circle.FadeColour(completeColour, duration);
|
||||
glow.FadeColour(completeColour, duration);
|
||||
transformFillColour(completeColour, 200);
|
||||
}
|
||||
|
||||
if (userTriggered || Time.Current < Spinner.EndTime)
|
||||
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 =>
|
||||
{
|
||||
if (Progress >= 1)
|
||||
@@ -191,8 +232,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
circle.Rotation = Disc.Rotation;
|
||||
Ticks.Rotation = Disc.Rotation;
|
||||
|
||||
SpmCounter.SetRotation(Disc.CumulativeRotation);
|
||||
|
||||
updateBonusScore();
|
||||
|
||||
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
|
||||
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)));
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
circleContainer.ScaleTo(Spinner.Scale * 0.3f);
|
||||
circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint);
|
||||
circleContainer.ScaleTo(0);
|
||||
mainContainer.ScaleTo(0);
|
||||
|
||||
mainContainer
|
||||
.ScaleTo(0)
|
||||
.ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint)
|
||||
.Then()
|
||||
.ScaleTo(1, 500, Easing.OutQuint);
|
||||
using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
|
||||
{
|
||||
float phaseOneScale = Spinner.Scale * 0.7f;
|
||||
|
||||
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)
|
||||
{
|
||||
base.UpdateStateTransforms(state);
|
||||
|
||||
var sequence = this.Delay(Spinner.Duration).FadeOut(160);
|
||||
|
||||
switch (state)
|
||||
using (BeginDelayedSequence(Spinner.Duration, true))
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out);
|
||||
break;
|
||||
this.FadeOut(160);
|
||||
|
||||
case ArmedState.Miss:
|
||||
sequence.ScaleTo(Scale * 0.8f, 320, Easing.In);
|
||||
break;
|
||||
switch (state)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
private readonly Slider slider;
|
||||
private readonly Drawable followCircle;
|
||||
private readonly DrawableSlider drawableSlider;
|
||||
private readonly CircularContainer ball;
|
||||
private readonly Drawable ball;
|
||||
|
||||
public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
|
||||
{
|
||||
@@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
Alpha = 0,
|
||||
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,
|
||||
Alpha = 1,
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()),
|
||||
}
|
||||
}
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private class FollowCircleContainer : Container
|
||||
private class FollowCircleContainer : CircularContainer
|
||||
{
|
||||
public override bool HandlePositionalInput => true;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// 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 osuTK.Graphics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class SpinnerBackground : CircularContainer, IHasAccentColour
|
||||
{
|
||||
protected Box Disc;
|
||||
public readonly Box Disc;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
|
||||
@@ -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 = $"{1000 * count}";
|
||||
bonusCounter.FadeOutFromOne(1500);
|
||||
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -9,10 +10,11 @@ using osu.Framework.Graphics.Effects;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class SpinnerTicks : Container
|
||||
public class SpinnerTicks : Container, IHasAccentColour
|
||||
{
|
||||
public SpinnerTicks()
|
||||
{
|
||||
@@ -20,28 +22,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
Anchor = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
const float count = 18;
|
||||
const float count = 8;
|
||||
|
||||
for (float i = 0; i < count; i++)
|
||||
{
|
||||
Add(new Container
|
||||
{
|
||||
Colour = Color4.Black,
|
||||
Alpha = 0.4f,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Radius = 10,
|
||||
Colour = Color4.Gray.Opacity(0.2f),
|
||||
},
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Size = new Vector2(60, 10),
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(
|
||||
0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f,
|
||||
0.5f + MathF.Cos(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.83f
|
||||
),
|
||||
Rotation = -i / count * 360 + 90,
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
using System;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@@ -26,14 +26,43 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
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.
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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 SpinnerBonusTick()
|
||||
{
|
||||
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
|
||||
|
||||
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
|
||||
{
|
||||
protected override int NumericResultFor(HitResult result) => 1100;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using 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 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) => 100;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
/// </summary>
|
||||
protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
protected const float SPIN_RADIUS = 50;
|
||||
public const float SPIN_RADIUS = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The time in ms between each ReplayFrame.
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
new MousePositionAbsoluteInput
|
||||
{
|
||||
Position = GamefieldToScreenSpace(Position ?? Vector2.Zero)
|
||||
},
|
||||
new ReplayState<OsuAction>
|
||||
{
|
||||
PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>()
|
||||
}
|
||||
};
|
||||
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
|
||||
inputs.Add(new ReplayState<OsuAction> { PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
private readonly Drawable animationContent;
|
||||
|
||||
private Sprite layerNd;
|
||||
private Sprite layerSpec;
|
||||
|
||||
public LegacySliderBall(Drawable animationContent)
|
||||
{
|
||||
this.animationContent = animationContent;
|
||||
@@ -29,18 +32,37 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new Sprite
|
||||
layerNd = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = skin.GetTexture("sliderb-nd"),
|
||||
Colour = new Color4(5, 5, 5, 255),
|
||||
},
|
||||
animationContent,
|
||||
new Sprite
|
||||
animationContent.With(d =>
|
||||
{
|
||||
d.Anchor = Anchor.Centre;
|
||||
d.Origin = Anchor.Centre;
|
||||
}),
|
||||
layerSpec = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = skin.GetTexture("sliderb-spec"),
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,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.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);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
[TestCase("basic")]
|
||||
[TestCase("slider-generating-drumroll")]
|
||||
[TestCase("sample-to-type-conversions")]
|
||||
[TestCase("slider-conversion-v6")]
|
||||
[TestCase("slider-conversion-v14")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void TestSpinnerDoesNotFail()
|
||||
public void TestSpinnerDoesFail()
|
||||
{
|
||||
bool judged = false;
|
||||
AddStep("Setup judgements", () =>
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Player.ScoreProcessor.NewJudgement += b => judged = true;
|
||||
});
|
||||
AddUntilStep("swell judged", () => judged);
|
||||
AddAssert("not failed", () => !Player.HasFailed);
|
||||
AddAssert("failed", () => Player.HasFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
@@ -82,37 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
case IHasDistance distanceData:
|
||||
{
|
||||
// Number of spans of the object - one for the initial length and for each repeat
|
||||
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)
|
||||
if (shouldConvertSliderToHits(obj, beatmap, distanceData, out var taikoDuration, out var tickSpacing))
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
double addition = 1;
|
||||
|
||||
// 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))
|
||||
addition += 0.75;
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
|
||||
{
|
||||
public class TaikoDrumRollJudgement : TaikoJudgement
|
||||
{
|
||||
public override bool AffectsCombo => false;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result)
|
||||
{
|
||||
// Drum rolls can be ignored with no health penalty
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
|
||||
{
|
||||
public class TaikoSwellJudgement : TaikoJudgement
|
||||
{
|
||||
public override bool AffectsCombo => false;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Replays
|
||||
|
||||
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>() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+379
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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:
|
||||
+137
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
internal class TaikoScoreProcessor : ScoreProcessor
|
||||
{
|
||||
protected override double DefaultAccuracyPortion => 0.75;
|
||||
|
||||
protected override double DefaultComboPortion => 0.25;
|
||||
|
||||
public override HitWindows CreateHitWindows() => new TaikoHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Skinning;
|
||||
@@ -34,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = skin.GetTexture("taiko-roll-end"),
|
||||
Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
|
||||
FillMode = FillMode.Fit,
|
||||
},
|
||||
body = new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = skin.GetTexture("taiko-roll-middle"),
|
||||
Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
|
||||
},
|
||||
headCircle = new LegacyCirclePiece
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
@@ -118,7 +119,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
@@ -216,7 +217,7 @@ namespace osu.Game.Tests.Skins
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -175,13 +175,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
|
||||
AddStep("Seek(49.999)", () => Clock.Seek(49.999));
|
||||
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("SeekForward, Snap", () => Clock.SeekForward(true));
|
||||
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
|
||||
AddStep("Seek(99.999)", () => Clock.Seek(99.999));
|
||||
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("SeekForward, Snap", () => Clock.SeekForward(true));
|
||||
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
@@ -13,6 +14,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
public class TestSceneTimingScreen : EditorClockTestScene
|
||||
{
|
||||
[Cached(typeof(EditorBeatmap))]
|
||||
[Cached(typeof(IBeatSnapProvider))]
|
||||
private readonly EditorBeatmap editorBeatmap;
|
||||
|
||||
public TestSceneTimingScreen()
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.Break;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@@ -35,6 +36,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
|
||||
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(1);
|
||||
|
||||
@@ -54,5 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex);
|
||||
}
|
||||
|
||||
private void press(Key key)
|
||||
{
|
||||
InputManager.PressKey(key);
|
||||
InputManager.ReleaseKey(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
new MousePositionAbsoluteInput
|
||||
{
|
||||
Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
|
||||
},
|
||||
new ReplayState<TestAction>
|
||||
{
|
||||
PressedActions = CurrentFrame?.Actions ?? new List<TestAction>()
|
||||
}
|
||||
};
|
||||
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
|
||||
inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
new MousePositionAbsoluteInput
|
||||
{
|
||||
Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
|
||||
},
|
||||
new ReplayState<TestAction>
|
||||
{
|
||||
PressedActions = CurrentFrame?.Actions ?? new List<TestAction>()
|
||||
}
|
||||
};
|
||||
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
|
||||
inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
@@ -295,7 +296,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
: 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();
|
||||
|
||||
@@ -306,7 +307,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
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();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -65,11 +64,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void bindHandler(double delay = 0)
|
||||
{
|
||||
var roomScores = new List<RoomScore>();
|
||||
var roomScores = new List<MultiplayerScore>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
roomScores.Add(new RoomScore
|
||||
roomScores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = i,
|
||||
Accuracy = 0.9 - 0.01 * i,
|
||||
|
||||
@@ -30,12 +30,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Add(selector = new SpotlightSelector());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibility()
|
||||
{
|
||||
AddStep("Toggle Visibility", selector.ToggleVisibility);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalSpotlights()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Rankings;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
onLoadStarted();
|
||||
|
||||
request = new GetSpotlightRankingsRequest(ruleset, spotlight);
|
||||
request = new GetSpotlightRankingsRequest(ruleset, spotlight, RankingsSortCriteria.All);
|
||||
((GetSpotlightRankingsRequest)request).Success += rankings => Schedule(() =>
|
||||
{
|
||||
var table = new ScoresTable(1, rankings.Users);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Spacing = new Vector2(10f),
|
||||
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
|
||||
{
|
||||
Username = @"flyte",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 TestSceneHueAnimation : OsuTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LargeTextureStore textures)
|
||||
{
|
||||
HueAnimation anim2;
|
||||
|
||||
Add(anim2 = new HueAnimation
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Texture = textures.Get("Intro/Triangles/logo-highlight"),
|
||||
Colour = Colour4.White,
|
||||
});
|
||||
|
||||
HueAnimation anim;
|
||||
|
||||
Add(anim = new HueAnimation
|
||||
{
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@@ -99,6 +101,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
public void TestManiaMods()
|
||||
{
|
||||
changeRuleset(3);
|
||||
|
||||
var mania = new ManiaRuleset();
|
||||
|
||||
testModsWithSameBaseType(
|
||||
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)),
|
||||
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -197,6 +205,18 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
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 selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));
|
||||
|
||||
@@ -36,11 +36,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}
|
||||
});
|
||||
|
||||
addHeader("Orange OverlayHeader (no background)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
|
||||
addHeader("Blue OverlayHeader", new TestNoControlHeader(), OverlayColourScheme.Blue);
|
||||
addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
|
||||
addHeader("Blue OverlayHeader (default 50 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue);
|
||||
addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green);
|
||||
addHeader("Pink TabControlOverlayHeader (enum)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
|
||||
addHeader("Red BreadcrumbControlOverlayHeader (no background)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
|
||||
addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
|
||||
addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
|
||||
}
|
||||
|
||||
private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme)
|
||||
@@ -86,6 +86,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private class TestNoBackgroundHeader : OverlayHeader
|
||||
{
|
||||
protected override OverlayTitle CreateTitle() => new TestTitle();
|
||||
|
||||
public TestNoBackgroundHeader()
|
||||
{
|
||||
ContentSidePadding = 100;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestNoControlHeader : OverlayHeader
|
||||
@@ -112,6 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
private class TestEnumTabControlHeader : TabControlOverlayHeader<TestEnum>
|
||||
{
|
||||
public TestEnumTabControlHeader()
|
||||
{
|
||||
ContentSidePadding = 30;
|
||||
}
|
||||
|
||||
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
|
||||
|
||||
protected override OverlayTitle CreateTitle() => new TestTitle();
|
||||
@@ -130,6 +140,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
public TestBreadcrumbControlHeader()
|
||||
{
|
||||
ContentSidePadding = 10;
|
||||
|
||||
TabControl.AddItem("tab1");
|
||||
TabControl.AddItem("tab2");
|
||||
TabControl.Current.Value = "tab2";
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
beatmap.BeatmapInfo = original.BeatmapInfo;
|
||||
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;
|
||||
|
||||
return beatmap;
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public class BeatmapDifficultyManager : CompositeDrawable
|
||||
{
|
||||
// Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
|
||||
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager));
|
||||
|
||||
// A permanent cache to prevent re-computations.
|
||||
private readonly ConcurrentDictionary<DifficultyCacheLookup, StarDifficulty> difficultyCache = new ConcurrentDictionary<DifficultyCacheLookup, StarDifficulty>();
|
||||
|
||||
// All bindables that should be updated along with the current ruleset + mods.
|
||||
private readonly LockedWeakList<BindableStarDifficulty> trackedBindables = new LockedWeakList<BindableStarDifficulty>();
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> currentRuleset { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> currentMods { get; set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
currentRuleset.BindValueChanged(_ => updateTrackedBindables());
|
||||
currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> that follows the currently-selected ruleset and mods.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
|
||||
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
|
||||
trackedBindables.Add(bindable);
|
||||
return bindable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The bindable will not update to follow the currently-selected ruleset and mods.
|
||||
/// </remarks>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
|
||||
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
|
||||
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
|
||||
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops computing the star difficulty.</param>
|
||||
/// <returns>The <see cref="StarDifficulty"/>.</returns>
|
||||
public async Task<StarDifficulty> GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable<Mod> mods = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
|
||||
return existing;
|
||||
|
||||
return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken,
|
||||
TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
|
||||
/// <returns>The <see cref="StarDifficulty"/>.</returns>
|
||||
public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable<Mod> mods = null)
|
||||
{
|
||||
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
|
||||
return existing;
|
||||
|
||||
return computeDifficulty(key, beatmapInfo, rulesetInfo);
|
||||
}
|
||||
|
||||
private CancellationTokenSource trackedUpdateCancellationSource;
|
||||
|
||||
/// <summary>
|
||||
/// Updates all tracked <see cref="BindableStarDifficulty"/> using the current ruleset and mods.
|
||||
/// </summary>
|
||||
private void updateTrackedBindables()
|
||||
{
|
||||
trackedUpdateCancellationSource?.Cancel();
|
||||
trackedUpdateCancellationSource = new CancellationTokenSource();
|
||||
|
||||
foreach (var b in trackedBindables)
|
||||
{
|
||||
if (trackedUpdateCancellationSource.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken))
|
||||
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the value of a <see cref="BindableStarDifficulty"/> with a given ruleset + mods.
|
||||
/// </summary>
|
||||
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
|
||||
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to update with.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s to update with.</param>
|
||||
/// <param name="cancellationToken">A token that may be used to cancel this update.</param>
|
||||
private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
|
||||
{
|
||||
GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t =>
|
||||
{
|
||||
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
|
||||
Schedule(() =>
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
bindable.Value = t.Result;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="BindableStarDifficulty"/> and triggers an initial value update.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> that star difficulty should correspond to.</param>
|
||||
/// <param name="initialRulesetInfo">The initial <see cref="RulesetInfo"/> to get the difficulty with.</param>
|
||||
/// <param name="initialMods">The initial <see cref="Mod"/>s to get the difficulty with.</param>
|
||||
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||
/// <returns>The <see cref="BindableStarDifficulty"/>.</returns>
|
||||
private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable<Mod> initialMods,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
|
||||
updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken);
|
||||
return bindable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the difficulty defined by a <see cref="DifficultyCacheLookup"/> key, and stores it to the timed cache.
|
||||
/// </summary>
|
||||
/// <param name="key">The <see cref="DifficultyCacheLookup"/> that defines the computation parameters.</param>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to compute the difficulty of.</param>
|
||||
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to compute the difficulty with.</param>
|
||||
/// <returns>The <see cref="StarDifficulty"/>.</returns>
|
||||
private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo)
|
||||
{
|
||||
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
|
||||
rulesetInfo ??= beatmapInfo.Ruleset;
|
||||
|
||||
try
|
||||
{
|
||||
var ruleset = rulesetInfo.CreateInstance();
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo));
|
||||
var attributes = calculator.Calculate(key.Mods);
|
||||
|
||||
return difficultyCache[key] = new StarDifficulty(attributes.StarRating);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return difficultyCache[key] = new StarDifficulty(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve an existing difficulty for the combination.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/>.</param>
|
||||
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/>.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s.</param>
|
||||
/// <param name="existingDifficulty">The existing difficulty value, if present.</param>
|
||||
/// <param name="key">The <see cref="DifficultyCacheLookup"/> key that was used to perform this lookup. This can be further used to query <see cref="computeDifficulty"/>.</param>
|
||||
/// <returns>Whether an existing difficulty was found.</returns>
|
||||
private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable<Mod> mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key)
|
||||
{
|
||||
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
|
||||
rulesetInfo ??= beatmapInfo.Ruleset;
|
||||
|
||||
// Difficulty can only be computed if the beatmap and ruleset are locally available.
|
||||
if (beatmapInfo.ID == 0 || rulesetInfo.ID == null)
|
||||
{
|
||||
// If not, fall back to the existing star difficulty (e.g. from an online source).
|
||||
existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty);
|
||||
key = default;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods);
|
||||
return difficultyCache.TryGetValue(key, out existingDifficulty);
|
||||
}
|
||||
|
||||
private readonly struct DifficultyCacheLookup : IEquatable<DifficultyCacheLookup>
|
||||
{
|
||||
public readonly int BeatmapId;
|
||||
public readonly int RulesetId;
|
||||
public readonly Mod[] Mods;
|
||||
|
||||
public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable<Mod> mods)
|
||||
{
|
||||
BeatmapId = beatmapId;
|
||||
RulesetId = rulesetId;
|
||||
Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty<Mod>();
|
||||
}
|
||||
|
||||
public bool Equals(DifficultyCacheLookup other)
|
||||
=> BeatmapId == other.BeatmapId
|
||||
&& RulesetId == other.RulesetId
|
||||
&& Mods.SequenceEqual(other.Mods);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = new HashCode();
|
||||
|
||||
hashCode.Add(BeatmapId);
|
||||
hashCode.Add(RulesetId);
|
||||
foreach (var mod in Mods)
|
||||
hashCode.Add(mod.Acronym);
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
private class BindableStarDifficulty : Bindable<StarDifficulty>
|
||||
{
|
||||
public readonly BeatmapInfo Beatmap;
|
||||
public readonly CancellationToken CancellationToken;
|
||||
|
||||
public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct StarDifficulty
|
||||
{
|
||||
public readonly double Stars;
|
||||
|
||||
public StarDifficulty(double stars)
|
||||
{
|
||||
Stars = stars;
|
||||
|
||||
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,49 +64,49 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the difficulty control point at.</param>
|
||||
/// <returns>The difficulty control point.</returns>
|
||||
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time);
|
||||
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the effect control point that is active at <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the effect control point at.</param>
|
||||
/// <returns>The effect control point.</returns>
|
||||
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time);
|
||||
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the sound control point that is active at <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the sound control point at.</param>
|
||||
/// <returns>The sound control point.</returns>
|
||||
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
|
||||
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the timing control point that is active at <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the timing control point at.</param>
|
||||
/// <returns>The timing control point.</returns>
|
||||
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
|
||||
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the maximum BPM represented by any timing control point.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double BPMMaximum =>
|
||||
60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
|
||||
60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the minimum BPM represented by any timing control point.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double BPMMinimum =>
|
||||
60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
|
||||
60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the mode BPM (most common BPM) represented by the control points.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double BPMMode =>
|
||||
60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
|
||||
60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
|
||||
|
||||
/// <summary>
|
||||
/// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state.
|
||||
@@ -170,12 +170,12 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="list">The list to search.</param>
|
||||
/// <param name="time">The time to find the control point at.</param>
|
||||
/// <param name="prePoint">The control point to use when <paramref name="time"/> is before any control points. If null, a new control point will be constructed.</param>
|
||||
/// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
|
||||
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
|
||||
private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T prePoint = null)
|
||||
where T : ControlPoint, new()
|
||||
private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
|
||||
where T : ControlPoint
|
||||
{
|
||||
return binarySearch(list, time) ?? prePoint ?? new T();
|
||||
return binarySearch(list, time) ?? fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
public class DifficultyControlPoint : ControlPoint
|
||||
{
|
||||
public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
|
||||
{
|
||||
SpeedMultiplierBindable = { Disabled = true },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier at this control point.
|
||||
/// </summary>
|
||||
|
||||
@@ -7,6 +7,12 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
public class EffectControlPoint : ControlPoint
|
||||
{
|
||||
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
|
||||
{
|
||||
KiaiModeBindable = { Disabled = true },
|
||||
OmitFirstBarLineBindable = { Disabled = true }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether the first bar line of this control point is ignored.
|
||||
/// </summary>
|
||||
|
||||
@@ -10,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
public const string DEFAULT_BANK = "normal";
|
||||
|
||||
public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
|
||||
{
|
||||
SampleBankBindable = { Disabled = true },
|
||||
SampleVolumeBindable = { Disabled = true }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The default sample bank at this control point.
|
||||
/// </summary>
|
||||
|
||||
@@ -13,6 +13,21 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
|
||||
|
||||
/// <summary>
|
||||
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
|
||||
/// </summary>
|
||||
private const double default_beat_length = 60000.0 / 60.0;
|
||||
|
||||
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
|
||||
{
|
||||
BeatLengthBindable =
|
||||
{
|
||||
Value = default_beat_length,
|
||||
Disabled = true
|
||||
},
|
||||
TimeSignatureBindable = { Disabled = true }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The time signature at this control point.
|
||||
/// </summary>
|
||||
|
||||
@@ -369,7 +369,9 @@ namespace osu.Game.Beatmaps.Formats
|
||||
addControlPoint(time, controlPoint, true);
|
||||
}
|
||||
|
||||
addControlPoint(time, new LegacyDifficultyControlPoint
|
||||
#pragma warning disable 618
|
||||
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
SpeedMultiplier = speedMultiplier,
|
||||
}, timingChange);
|
||||
|
||||
@@ -159,11 +159,20 @@ namespace osu.Game.Beatmaps.Formats
|
||||
Mania,
|
||||
}
|
||||
|
||||
internal class LegacyDifficultyControlPoint : DifficultyControlPoint
|
||||
[Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
|
||||
public class LegacyDifficultyControlPoint : DifficultyControlPoint
|
||||
{
|
||||
public LegacyDifficultyControlPoint()
|
||||
/// <summary>
|
||||
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
|
||||
/// DO NOT USE THIS UNLESS 100% SURE.
|
||||
/// </summary>
|
||||
public readonly float BpmMultiplier;
|
||||
|
||||
public LegacyDifficultyControlPoint(double beatLength)
|
||||
{
|
||||
SpeedMultiplierBindable.Precision = double.Epsilon;
|
||||
|
||||
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ namespace osu.Game.Configuration
|
||||
Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
|
||||
|
||||
Set(OsuSetting.IncreaseFirstObjectVisibility, true);
|
||||
Set(OsuSetting.GameplayDisableWinKey, true);
|
||||
|
||||
// Update
|
||||
Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
||||
@@ -229,6 +230,7 @@ namespace osu.Game.Configuration
|
||||
IntroSequence,
|
||||
UIHoldActivationDelay,
|
||||
HitLighting,
|
||||
MenuBackgroundSource
|
||||
MenuBackgroundSource,
|
||||
GameplayDisableWinKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,14 +43,6 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
public double MinimumBeatLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
|
||||
/// </summary>
|
||||
private const double default_beat_length = 60000.0 / 60.0;
|
||||
|
||||
private TimingControlPoint defaultTiming;
|
||||
private EffectControlPoint defaultEffect;
|
||||
|
||||
protected bool IsBeatSyncedWithTrack { get; private set; }
|
||||
|
||||
protected override void Update()
|
||||
@@ -81,8 +73,8 @@ namespace osu.Game.Graphics.Containers
|
||||
if (timingPoint == null || !IsBeatSyncedWithTrack)
|
||||
{
|
||||
currentTrackTime = Clock.CurrentTime;
|
||||
timingPoint = defaultTiming;
|
||||
effectPoint = defaultEffect;
|
||||
timingPoint = TimingControlPoint.DEFAULT;
|
||||
effectPoint = EffectControlPoint.DEFAULT;
|
||||
}
|
||||
|
||||
double beatLength = timingPoint.BeatLength / Divisor;
|
||||
@@ -116,17 +108,6 @@ namespace osu.Game.Graphics.Containers
|
||||
private void load(IBindable<WorkingBeatmap> beatmap)
|
||||
{
|
||||
Beatmap.BindTo(beatmap);
|
||||
|
||||
defaultTiming = new TimingControlPoint
|
||||
{
|
||||
BeatLength = default_beat_length,
|
||||
};
|
||||
|
||||
defaultEffect = new EffectControlPoint
|
||||
{
|
||||
KiaiMode = false,
|
||||
OmitFirstBarLine = false
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@@ -55,7 +54,16 @@ namespace osu.Game.Graphics.Cursor
|
||||
return;
|
||||
}
|
||||
|
||||
var newTarget = inputManager.HoveredDrawables.OfType<IProvideCursor>().FirstOrDefault(t => t.ProvidingUserCursor) ?? this;
|
||||
IProvideCursor newTarget = this;
|
||||
|
||||
foreach (var d in inputManager.HoveredDrawables)
|
||||
{
|
||||
if (d is IProvideCursor p && p.ProvidingUserCursor)
|
||||
{
|
||||
newTarget = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTarget == newTarget)
|
||||
return;
|
||||
|
||||
@@ -19,6 +19,7 @@ using osu.Game.Input.Bindings;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
|
||||
namespace osu.Game.Graphics
|
||||
{
|
||||
@@ -119,7 +120,9 @@ namespace osu.Game.Graphics
|
||||
break;
|
||||
|
||||
case ScreenshotFormat.Jpg:
|
||||
image.SaveAsJpeg(stream);
|
||||
const int jpeg_quality = 92;
|
||||
|
||||
image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality });
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
public class HueAnimation : Sprite
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ShaderManager shaders, TextureStore textures)
|
||||
{
|
||||
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation");
|
||||
RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); // Masking isn't supported for now
|
||||
}
|
||||
|
||||
private float animationProgress;
|
||||
|
||||
public float AnimationProgress
|
||||
{
|
||||
get => animationProgress;
|
||||
set
|
||||
{
|
||||
if (animationProgress == value) return;
|
||||
|
||||
animationProgress = value;
|
||||
Invalidate(Invalidation.DrawInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsPresent => true;
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new HueAnimationDrawNode(this);
|
||||
|
||||
private class HueAnimationDrawNode : SpriteDrawNode
|
||||
{
|
||||
private HueAnimation source => (HueAnimation)Source;
|
||||
|
||||
private float progress;
|
||||
|
||||
public HueAnimationDrawNode(HueAnimation source)
|
||||
: base(source)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ApplyState()
|
||||
{
|
||||
base.ApplyState();
|
||||
|
||||
progress = source.animationProgress;
|
||||
}
|
||||
|
||||
protected override void Blit(Action<TexturedVertex2D> vertexAction)
|
||||
{
|
||||
Shader.GetUniform<float>("progress").UpdateValue(ref progress);
|
||||
|
||||
base.Blit(vertexAction);
|
||||
}
|
||||
|
||||
protected override bool CanDrawOpaqueInterior => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public bool OnPressed(GlobalAction action)
|
||||
{
|
||||
if (!HasFocus) return false;
|
||||
|
||||
if (action == GlobalAction.Back)
|
||||
{
|
||||
if (Text.Length > 0)
|
||||
|
||||
@@ -57,6 +57,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
|
||||
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
|
||||
@@ -160,6 +161,9 @@ namespace osu.Game.Input.Bindings
|
||||
Home,
|
||||
|
||||
[Description("Toggle notifications")]
|
||||
ToggleNotifications
|
||||
ToggleNotifications,
|
||||
|
||||
[Description("Pause")]
|
||||
PauseGameplay,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Overlays.Rankings;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
@@ -9,11 +10,13 @@ namespace osu.Game.Online.API.Requests
|
||||
public class GetSpotlightRankingsRequest : GetRankingsRequest<GetSpotlightRankingsResponse>
|
||||
{
|
||||
private readonly int spotlight;
|
||||
private readonly RankingsSortCriteria sort;
|
||||
|
||||
public GetSpotlightRankingsRequest(RulesetInfo ruleset, int spotlight)
|
||||
public GetSpotlightRankingsRequest(RulesetInfo ruleset, int spotlight, RankingsSortCriteria sort)
|
||||
: base(ruleset, 1)
|
||||
{
|
||||
this.spotlight = spotlight;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
@@ -21,6 +24,7 @@ namespace osu.Game.Online.API.Requests
|
||||
var req = base.CreateWebRequest();
|
||||
|
||||
req.AddParameter("spotlight", spotlight.ToString());
|
||||
req.AddParameter("filter", sort.ToString().ToLower());
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,9 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public class APICreatedRoom : Room
|
||||
{
|
||||
+1
-1
@@ -6,7 +6,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public class APIPlaylistBeatmap : APIBeatmap
|
||||
{
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public class APIScoreToken
|
||||
{
|
||||
+2
-3
@@ -4,10 +4,9 @@
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public class CreateRoomRequest : APIRequest<APICreatedRoom>
|
||||
{
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
|
||||
using System.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public class CreateRoomScoreRequest : APIRequest<APIScoreToken>
|
||||
{
|
||||
+3
-2
@@ -3,8 +3,9 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public class GetRoomPlaylistScoresRequest : APIRequest<RoomPlaylistScores>
|
||||
{
|
||||
@@ -23,6 +24,6 @@ namespace osu.Game.Online.API.Requests
|
||||
public class RoomPlaylistScores
|
||||
{
|
||||
[JsonProperty("scores")]
|
||||
public List<RoomScore> Scores { get; set; }
|
||||
public List<MultiplayerScore> Scores { get; set; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user