Compare commits
590 Commits
2020.710.0
...
2020.801.0
@@ -5,6 +5,6 @@
|
||||
"version": "3.1.100"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "2.0.50"
|
||||
"Microsoft.Build.Traversal": "2.0.52"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.622.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.710.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.730.1" />
|
||||
</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);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
<PackageReference Include="nunit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
|
||||
return 0;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 0.01;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
|
||||
@@ -8,8 +10,27 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
public class Banana : Fruit
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of banana in current shower.
|
||||
/// </summary>
|
||||
public int BananaIndex;
|
||||
|
||||
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
|
||||
|
||||
public override Judgement CreateJudgement() => new CatchBananaJudgement();
|
||||
|
||||
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };
|
||||
|
||||
public Banana()
|
||||
{
|
||||
Samples = samples;
|
||||
}
|
||||
|
||||
private class BananaHitSampleInfo : HitSampleInfo
|
||||
{
|
||||
private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };
|
||||
|
||||
public override IEnumerable<string> LookupNames => lookupNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,21 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
if (spacing <= 0)
|
||||
return;
|
||||
|
||||
for (double i = StartTime; i <= EndTime; i += spacing)
|
||||
double time = StartTime;
|
||||
int i = 0;
|
||||
|
||||
while (time <= EndTime)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AddNested(new Banana
|
||||
{
|
||||
Samples = Samples,
|
||||
StartTime = i
|
||||
StartTime = time,
|
||||
BananaIndex = i,
|
||||
});
|
||||
|
||||
time += spacing;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1);
|
||||
}
|
||||
|
||||
public override void PlaySamples()
|
||||
{
|
||||
base.PlaySamples();
|
||||
if (Samples != null)
|
||||
Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
|
||||
}
|
||||
|
||||
private Color4 getBananaColour()
|
||||
{
|
||||
switch (RNG.Next(0, 3))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneHitExplosion : ManiaSkinnableTestScene
|
||||
{
|
||||
private readonly List<DrawablePool<PoolableHitExplosion>> hitExplosionPools = new List<DrawablePool<PoolableHitExplosion>>();
|
||||
|
||||
public TestSceneHitExplosion()
|
||||
{
|
||||
int runcount = 0;
|
||||
@@ -29,28 +33,40 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
if (runcount % 15 > 12)
|
||||
return;
|
||||
|
||||
CreatedDrawables.OfType<Container>().ForEach(c =>
|
||||
int poolIndex = 0;
|
||||
|
||||
foreach (var c in CreatedDrawables.OfType<Container>())
|
||||
{
|
||||
c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, 0),
|
||||
_ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}));
|
||||
});
|
||||
c.Add(hitExplosionPools[poolIndex].Get(e =>
|
||||
{
|
||||
e.Apply(new JudgementResult(new HitObject(), runcount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
|
||||
|
||||
e.Anchor = Anchor.Centre;
|
||||
e.Origin = Anchor.Centre;
|
||||
}));
|
||||
|
||||
poolIndex++;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1)
|
||||
SetContents(() =>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.Y,
|
||||
Y = -0.25f,
|
||||
Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
|
||||
var pool = new DrawablePool<PoolableHitExplosion>(5);
|
||||
hitExplosionPools.Add(pool);
|
||||
|
||||
return new ColumnTestContainer(0, ManiaAction.Key1)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.Y,
|
||||
Y = -0.25f,
|
||||
Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
|
||||
Child = pool
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
|
||||
return 300;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return 320;
|
||||
return 350;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.95;
|
||||
|
||||
protected override double DefaultComboPortion => 0.05;
|
||||
|
||||
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;
|
||||
|
||||
@@ -6,13 +6,15 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
public class LegacyHitExplosion : LegacyManiaColumnElement
|
||||
public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
|
||||
{
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
@@ -62,9 +64,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
public void Animate(JudgementResult result)
|
||||
{
|
||||
base.LoadComplete();
|
||||
(explosion as IFramedAnimation)?.GotoFrame(0);
|
||||
|
||||
explosion?.FadeInFromZero(80)
|
||||
.Then().FadeOut(120);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI.Components;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
@@ -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;
|
||||
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
||||
|
||||
public Container UnderlayElements => hitObjectArea.UnderlayElements;
|
||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||
|
||||
public Column(int index)
|
||||
{
|
||||
@@ -53,9 +53,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5),
|
||||
// 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 +65,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;
|
||||
@@ -108,15 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ =>
|
||||
new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
|
||||
hitObjectArea.Explosions.Add(explosion);
|
||||
|
||||
explosion.Delay(200).Expire(true);
|
||||
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
|
||||
}
|
||||
|
||||
public bool OnPressed(ManiaAction action)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
@@ -15,35 +17,36 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public class DefaultHitExplosion : CompositeDrawable
|
||||
public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
|
||||
{
|
||||
private const float default_large_faint_size = 0.8f;
|
||||
|
||||
public override bool RemoveWhenNotAlive => true;
|
||||
|
||||
[Resolved]
|
||||
private Column column { get; set; }
|
||||
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private readonly CircularContainer largeFaint;
|
||||
private readonly CircularContainer mainGlow1;
|
||||
private CircularContainer largeFaint;
|
||||
private CircularContainer mainGlow1;
|
||||
|
||||
public DefaultHitExplosion(Color4 objectColour, bool isSmall = false)
|
||||
public DefaultHitExplosion()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = DefaultNotePiece.NOTE_HEIGHT;
|
||||
}
|
||||
|
||||
// scale roughly in-line with visual appearance of notes
|
||||
Scale = new Vector2(1f, 0.6f);
|
||||
|
||||
if (isSmall)
|
||||
Scale *= 0.5f;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
const float angle_variangle = 15; // should be less than 45
|
||||
|
||||
const float roundness = 80;
|
||||
|
||||
const float initial_height = 10;
|
||||
|
||||
var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
|
||||
var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -54,12 +57,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
// we want our size to be very small so the glow dominates it.
|
||||
Size = new Vector2(0.8f),
|
||||
Size = new Vector2(default_large_faint_size),
|
||||
Blending = BlendingParameters.Additive,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
|
||||
Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f),
|
||||
Roundness = 160,
|
||||
Radius = 200,
|
||||
},
|
||||
@@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
|
||||
Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1),
|
||||
Roundness = 20,
|
||||
Radius = 50,
|
||||
},
|
||||
@@ -114,30 +117,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
const double duration = 200;
|
||||
|
||||
base.LoadComplete();
|
||||
|
||||
largeFaint
|
||||
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
|
||||
.FadeOut(duration * 2);
|
||||
|
||||
mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint);
|
||||
|
||||
this.FadeOut(duration, Easing.Out);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
if (direction.NewValue == ScrollingDirection.Up)
|
||||
@@ -151,5 +135,29 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Y = -DefaultNotePiece.NOTE_HEIGHT / 2;
|
||||
}
|
||||
}
|
||||
|
||||
public void Animate(JudgementResult result)
|
||||
{
|
||||
// scale roughly in-line with visual appearance of notes
|
||||
Vector2 scale = new Vector2(1, 0.6f);
|
||||
|
||||
if (result.Judgement is HoldNoteTickJudgement)
|
||||
scale *= 0.5f;
|
||||
|
||||
this.ScaleTo(scale);
|
||||
|
||||
largeFaint
|
||||
.ResizeTo(default_large_faint_size)
|
||||
.Then()
|
||||
.ResizeTo(default_large_faint_size * new Vector2(5, 1), PoolableHitExplosion.DURATION, Easing.OutQuint)
|
||||
.FadeOut(PoolableHitExplosion.DURATION * 2);
|
||||
|
||||
mainGlow1
|
||||
.ScaleTo(1)
|
||||
.Then()
|
||||
.ScaleTo(1.4f, PoolableHitExplosion.DURATION, Easing.OutQuint);
|
||||
|
||||
this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableManiaJudgement()
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
||||
@@ -0,0 +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 osu.Game.Rulesets.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Common interface for all hit explosion bodies.
|
||||
/// </summary>
|
||||
public interface IHitExplosion
|
||||
{
|
||||
/// <summary>
|
||||
/// Begins animating this <see cref="IHitExplosion"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The type of <see cref="JudgementResult"/> that caused this explosion.</param>
|
||||
void Animate(JudgementResult result);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public class PoolableHitExplosion : PoolableDrawable
|
||||
{
|
||||
public const double DURATION = 200;
|
||||
|
||||
public JudgementResult Result { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private Column column { get; set; }
|
||||
|
||||
private SkinnableDrawable skinnableExplosion;
|
||||
|
||||
public PoolableHitExplosion()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
}
|
||||
|
||||
public void Apply(JudgementResult result)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
(skinnableExplosion?.Drawable as IHitExplosion)?.Animate(Result);
|
||||
|
||||
this.Delay(DURATION).Then().Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@@ -33,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
public IReadOnlyList<Column> Columns => columnFlow.Children;
|
||||
private readonly FillFlowContainer<Column> columnFlow;
|
||||
|
||||
public Container<DrawableManiaJudgement> Judgements => judgements;
|
||||
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
||||
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
|
||||
|
||||
private readonly Drawable barLineContainer;
|
||||
private readonly Container topLevelContainer;
|
||||
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -208,12 +210,14 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
judgements.Clear();
|
||||
judgements.Add(new DrawableManiaJudgement(result, judgedObject)
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPool.Get(j =>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
}));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public abstract class OsuSkinnableTestScene : SkinnableTestScene
|
||||
{
|
||||
private Container content;
|
||||
|
||||
protected override Container<Drawable> Content
|
||||
{
|
||||
get
|
||||
{
|
||||
if (content == null)
|
||||
base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -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)
|
||||
{
|
||||
|
||||
@@ -2,29 +2,111 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneDrawableJudgement : OsuSkinnableTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
private readonly List<DrawablePool<TestDrawableOsuJudgement>> pools;
|
||||
|
||||
public TestSceneDrawableJudgement()
|
||||
{
|
||||
pools = new List<DrawablePool<TestDrawableOsuJudgement>>();
|
||||
|
||||
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Skip(1))
|
||||
showResult(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitLightingDisabled()
|
||||
{
|
||||
AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false));
|
||||
|
||||
showResult(HitResult.Great);
|
||||
|
||||
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
|
||||
AddAssert("judgement body immediately visible",
|
||||
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha == 1));
|
||||
AddAssert("hit lighting hidden",
|
||||
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha == 0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitLightingEnabled()
|
||||
{
|
||||
AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true));
|
||||
|
||||
showResult(HitResult.Great);
|
||||
|
||||
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
|
||||
AddAssert("judgement body not immediately visible",
|
||||
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
|
||||
AddAssert("hit lighting shown",
|
||||
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha > 0));
|
||||
}
|
||||
|
||||
private void showResult(HitResult result)
|
||||
{
|
||||
AddStep("Show " + result.GetDescription(), () =>
|
||||
{
|
||||
AddStep("Show " + result.GetDescription(), () => SetContents(() =>
|
||||
new DrawableOsuJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
|
||||
int poolIndex = 0;
|
||||
|
||||
SetContents(() =>
|
||||
{
|
||||
DrawablePool<TestDrawableOsuJudgement> pool;
|
||||
|
||||
if (poolIndex >= pools.Count)
|
||||
pools.Add(pool = new DrawablePool<TestDrawableOsuJudgement>(1));
|
||||
else
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}));
|
||||
}
|
||||
pool = pools[poolIndex];
|
||||
|
||||
// We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
|
||||
((Container)pool.Parent).Clear(false);
|
||||
}
|
||||
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
pool,
|
||||
pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
|
||||
{
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
poolIndex++;
|
||||
return container;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private class TestDrawableOsuJudgement : DrawableOsuJudgement
|
||||
{
|
||||
public new SkinnableSprite Lighting => base.Lighting;
|
||||
public new Container JudgementBody => base.JudgementBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -26,19 +25,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestFixture]
|
||||
public class TestSceneSlider : OsuSkinnableTestScene
|
||||
{
|
||||
private Container content;
|
||||
|
||||
protected override Container<Drawable> Content
|
||||
{
|
||||
get
|
||||
{
|
||||
if (content == null)
|
||||
base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
private int depthIndex;
|
||||
|
||||
public TestSceneSlider()
|
||||
|
||||
@@ -4,39 +4,32 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneSpinner : OsuTestScene
|
||||
public class TestSceneSpinner : OsuSkinnableTestScene
|
||||
{
|
||||
private readonly Container content;
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private int depthIndex;
|
||||
|
||||
public TestSceneSpinner()
|
||||
{
|
||||
base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
|
||||
|
||||
AddStep("Miss Big", () => testSingle(2));
|
||||
AddStep("Miss Medium", () => testSingle(5));
|
||||
AddStep("Miss Small", () => testSingle(7));
|
||||
AddStep("Hit Big", () => testSingle(2, true));
|
||||
AddStep("Hit Medium", () => testSingle(5, true));
|
||||
AddStep("Hit Small", () => testSingle(7, true));
|
||||
AddStep("Miss Big", () => SetContents(() => testSingle(2)));
|
||||
AddStep("Miss Medium", () => SetContents(() => testSingle(5)));
|
||||
AddStep("Miss Small", () => SetContents(() => testSingle(7)));
|
||||
AddStep("Hit Big", () => SetContents(() => testSingle(2, true)));
|
||||
AddStep("Hit Medium", () => SetContents(() => testSingle(5, true)));
|
||||
AddStep("Hit Small", () => SetContents(() => testSingle(7, true)));
|
||||
}
|
||||
|
||||
private void testSingle(float circleSize, bool auto = false)
|
||||
private Drawable 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 });
|
||||
|
||||
@@ -49,12 +42,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
|
||||
mod.ApplyToDrawableHitObjects(new[] { drawable });
|
||||
|
||||
Add(drawable);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
private class TestDrawableSpinner : DrawableSpinner
|
||||
{
|
||||
private bool auto;
|
||||
private readonly bool auto;
|
||||
|
||||
public TestDrawableSpinner(Spinner s, bool auto)
|
||||
: base(s)
|
||||
@@ -62,16 +55,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
this.auto = auto;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
protected override void Update()
|
||||
{
|
||||
if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1)
|
||||
{
|
||||
// force completion only once to not break human interaction
|
||||
Disc.CumulativeRotation = Spinner.SpinsRequired * 360;
|
||||
auto = false;
|
||||
}
|
||||
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
base.Update();
|
||||
if (auto)
|
||||
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -57,12 +61,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public void TestSpinnerRewindingRotation()
|
||||
{
|
||||
addSeekStep(5000);
|
||||
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
|
||||
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
|
||||
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
|
||||
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
|
||||
|
||||
addSeekStep(0);
|
||||
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
|
||||
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
|
||||
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
|
||||
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -71,24 +75,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0;
|
||||
|
||||
addSeekStep(5000);
|
||||
AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation);
|
||||
AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.CumulativeRotation);
|
||||
AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.RotationTracker.Rotation);
|
||||
AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.RotationTracker.CumulativeRotation);
|
||||
AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation);
|
||||
|
||||
addSeekStep(2500);
|
||||
AddUntilStep("disc rotation rewound",
|
||||
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
|
||||
() => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation / 2, 100));
|
||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalRelativeDiscRotation / 2, 100));
|
||||
AddUntilStep("symbol rotation rewound",
|
||||
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100));
|
||||
|
||||
addSeekStep(5000);
|
||||
AddAssert("is disc rotation almost same",
|
||||
() => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation, 100));
|
||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalRelativeDiscRotation, 100));
|
||||
AddAssert("is symbol rotation almost same",
|
||||
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100));
|
||||
AddAssert("is disc rotation absolute almost same",
|
||||
() => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, finalAbsoluteDiscRotation, 100));
|
||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalAbsoluteDiscRotation, 100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -111,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addSeekStep(5000);
|
||||
|
||||
AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.Disc.Rotation > 0 : drawableSpinner.Disc.Rotation < 0);
|
||||
AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0);
|
||||
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
|
||||
}
|
||||
|
||||
@@ -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.RotationTracker.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK;
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
|
||||
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpinnerCompleteBonusRewinding()
|
||||
{
|
||||
addSeekStep(2500);
|
||||
addSeekStep(0);
|
||||
|
||||
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private OsuInputManager inputManager;
|
||||
|
||||
private GameplayClock gameplayClock;
|
||||
|
||||
private List<OsuReplayFrame> replayFrames;
|
||||
|
||||
private int currentFrame;
|
||||
@@ -38,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (currentFrame == replayFrames.Count - 1) return;
|
||||
|
||||
double time = playfield.Time.Current;
|
||||
double time = gameplayClock.CurrentTime;
|
||||
|
||||
// Very naive implementation of autopilot based on proximity to replay frames.
|
||||
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
|
||||
@@ -53,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
|
||||
// Grab the input manager to disable the user's cursor, and for future use
|
||||
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
|
||||
inputManager.AllowUserCursorMovement = false;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override string Description => "Hit them at the right size!";
|
||||
|
||||
protected override float StartScale => 2f;
|
||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||
public override BindableNumber<float> StartScale { get; } = new BindableFloat
|
||||
{
|
||||
MinValue = 1f,
|
||||
MaxValue = 25f,
|
||||
Default = 2f,
|
||||
Value = 2f,
|
||||
Precision = 0.1f,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public BindableNumber<float> CircleSize { get; } = new BindableFloat
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 1,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public BindableNumber<float> ApproachRate { get; } = new BindableFloat
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 1,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override string Description => "Hit them at the right size!";
|
||||
|
||||
protected override float StartScale => 0.5f;
|
||||
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
|
||||
public override BindableNumber<float> StartScale { get; } = new BindableFloat
|
||||
{
|
||||
MinValue = 0f,
|
||||
MaxValue = 0.99f,
|
||||
Default = 0.5f,
|
||||
Value = 0.5f,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
case DrawableSpinner spinner:
|
||||
// hide elements we don't care about.
|
||||
spinner.Disc.Hide();
|
||||
spinner.Ticks.Hide();
|
||||
spinner.Background.Hide();
|
||||
// todo: hide background
|
||||
|
||||
using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true))
|
||||
spinner.FadeOut(fadeOutDuration);
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
protected virtual float StartScale => 1;
|
||||
public abstract BindableNumber<float> StartScale { get; }
|
||||
|
||||
protected virtual float EndScale => 1;
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
case DrawableHitCircle _:
|
||||
{
|
||||
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
|
||||
drawable.ScaleTo(StartScale).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
|
||||
drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
var spinner = (DrawableSpinner)drawable;
|
||||
|
||||
spinner.Disc.Tracking = true;
|
||||
spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
|
||||
spinner.RotationTracker.Tracking = true;
|
||||
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
var h = drawableOsu.HitObject;
|
||||
|
||||
//todo: expose and hide spinner background somehow
|
||||
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
@@ -56,11 +58,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
slider.Body.OnSkinChanged += () => applySliderState(slider);
|
||||
applySliderState(slider);
|
||||
break;
|
||||
|
||||
case DrawableSpinner spinner:
|
||||
spinner.Disc.Hide();
|
||||
spinner.Background.Hide();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableOsuJudgement : DrawableJudgement
|
||||
{
|
||||
private SkinnableSprite lighting;
|
||||
protected SkinnableSprite Lighting;
|
||||
|
||||
private Bindable<Color4> lightingColour;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
|
||||
: base(result, judgedObject)
|
||||
{
|
||||
@@ -29,18 +33,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
private void load()
|
||||
{
|
||||
if (config.Get<bool>(OsuSetting.HitLighting))
|
||||
AddInternal(Lighting = new SkinnableSprite("lighting")
|
||||
{
|
||||
AddInternal(lighting = new SkinnableSprite("lighting")
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
}
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Depth = float.MaxValue,
|
||||
Alpha = 0
|
||||
});
|
||||
}
|
||||
|
||||
public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
|
||||
@@ -60,31 +62,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
lightingColour?.UnbindAll();
|
||||
|
||||
if (lighting != null)
|
||||
Lighting.ResetAnimation();
|
||||
|
||||
if (JudgedObject != null)
|
||||
{
|
||||
if (JudgedObject != null)
|
||||
{
|
||||
lightingColour = JudgedObject.AccentColour.GetBoundCopy();
|
||||
lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
lighting.Colour = Color4.White;
|
||||
}
|
||||
lightingColour = JudgedObject.AccentColour.GetBoundCopy();
|
||||
lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Lighting.Colour = Color4.White;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double FadeOutDelay => lighting == null ? base.FadeOutDelay : 1400;
|
||||
private double fadeOutDelay;
|
||||
protected override double FadeOutDelay => fadeOutDelay;
|
||||
|
||||
protected override void ApplyHitAnimations()
|
||||
{
|
||||
if (lighting != null)
|
||||
bool hitLightingEnabled = config.Get<bool>(OsuSetting.HitLighting);
|
||||
|
||||
if (hitLightingEnabled)
|
||||
{
|
||||
JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
|
||||
|
||||
lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
|
||||
lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
|
||||
Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
|
||||
Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
JudgementBody.Alpha = 1;
|
||||
}
|
||||
|
||||
fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
|
||||
|
||||
JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
|
||||
base.ApplyHitAnimations();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -3,20 +3,18 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@@ -24,27 +22,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
protected readonly Spinner Spinner;
|
||||
|
||||
public readonly SpinnerDisc Disc;
|
||||
public readonly SpinnerTicks Ticks;
|
||||
private readonly Container<DrawableSpinnerTick> ticks;
|
||||
|
||||
public readonly SpinnerRotationTracker RotationTracker;
|
||||
public readonly SpinnerSpmCounter SpmCounter;
|
||||
|
||||
private readonly Container mainContainer;
|
||||
|
||||
public readonly SpinnerBackground Background;
|
||||
private readonly Container circleContainer;
|
||||
private readonly CirclePiece circle;
|
||||
private readonly GlowPiece glow;
|
||||
|
||||
private readonly SpriteIcon symbol;
|
||||
|
||||
private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c");
|
||||
private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c");
|
||||
private readonly SpinnerBonusDisplay bonusDisplay;
|
||||
|
||||
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
|
||||
|
||||
private Color4 normalColour;
|
||||
private Color4 completeColour;
|
||||
|
||||
public DrawableSpinner(Spinner s)
|
||||
: base(s)
|
||||
{
|
||||
@@ -53,62 +38,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
// we are slightly bigger than our parent, to clip the top and bottom of the circle
|
||||
Height = 1.3f;
|
||||
|
||||
Spinner = s;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
circleContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
glow = new GlowPiece(),
|
||||
circle = new CirclePiece
|
||||
{
|
||||
Position = Vector2.Zero,
|
||||
Anchor = Anchor.Centre,
|
||||
},
|
||||
new RingPiece(),
|
||||
symbol = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(48),
|
||||
Icon = FontAwesome.Solid.Asterisk,
|
||||
Shadow = false,
|
||||
},
|
||||
}
|
||||
},
|
||||
mainContainer = new AspectContainer
|
||||
ticks = new Container<DrawableSpinnerTick>(),
|
||||
new AspectContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Children = new[]
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Background = new SpinnerBackground
|
||||
{
|
||||
Alpha = 0.6f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
Disc = new SpinnerDisc(Spinner)
|
||||
{
|
||||
Scale = Vector2.Zero,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
circleContainer.CreateProxy(),
|
||||
Ticks = new SpinnerTicks
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()),
|
||||
RotationTracker = new SpinnerRotationTracker(Spinner)
|
||||
}
|
||||
},
|
||||
SpmCounter = new SpinnerSpmCounter
|
||||
@@ -117,51 +60,136 @@ 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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Bindable<bool> isSpinning;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
isSpinning = RotationTracker.IsSpinning.GetBoundCopy();
|
||||
isSpinning.BindValueChanged(updateSpinningSample);
|
||||
}
|
||||
|
||||
private SkinnableSound spinningSample;
|
||||
|
||||
private const float minimum_volume = 0.0001f;
|
||||
|
||||
protected override void LoadSamples()
|
||||
{
|
||||
base.LoadSamples();
|
||||
|
||||
spinningSample?.Expire();
|
||||
spinningSample = null;
|
||||
|
||||
var firstSample = HitObject.Samples.FirstOrDefault();
|
||||
|
||||
if (firstSample != null)
|
||||
{
|
||||
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
|
||||
clone.Name = "spinnerspin";
|
||||
|
||||
AddInternal(spinningSample = new SkinnableSound(clone)
|
||||
{
|
||||
Volume = { Value = minimum_volume },
|
||||
Looping = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSpinningSample(ValueChangedEvent<bool> tracking)
|
||||
{
|
||||
// note that samples will not start playing if exiting a seek operation in the middle of a spinner.
|
||||
// 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)
|
||||
{
|
||||
spinningSample?.Play();
|
||||
spinningSample?.VolumeTo(1, 200);
|
||||
}
|
||||
else
|
||||
{
|
||||
spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
base.AddNestedHitObject(hitObject);
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableSpinnerTick tick:
|
||||
ticks.Add(tick);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateStateTransforms(ArmedState state)
|
||||
{
|
||||
base.UpdateStateTransforms(state);
|
||||
|
||||
using (BeginDelayedSequence(Spinner.Duration, true))
|
||||
this.FadeOut(160);
|
||||
|
||||
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
|
||||
isSpinning?.TriggerChange();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Background.AccentColour = normalColour;
|
||||
|
||||
completeColour = colours.YellowLight.Opacity(0.75f);
|
||||
|
||||
Disc.AccentColour = fillColour;
|
||||
circle.Colour = colours.BlueDark;
|
||||
glow.Colour = colours.BlueDark;
|
||||
|
||||
positionBindable.BindValueChanged(pos => Position = pos.NewValue);
|
||||
positionBindable.BindTo(HitObject.PositionBindable);
|
||||
}
|
||||
|
||||
public float Progress => Math.Clamp(Disc.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
|
||||
/// <summary>
|
||||
/// The completion progress of this spinner from 0..1 (clamped).
|
||||
/// </summary>
|
||||
public float Progress => Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (Time.Current < HitObject.StartTime) return;
|
||||
|
||||
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);
|
||||
}
|
||||
RotationTracker.Complete.Value = Progress >= 1;
|
||||
|
||||
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)
|
||||
@@ -178,57 +206,55 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (HandleUserInput)
|
||||
Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
|
||||
RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
|
||||
|
||||
if (spinningSample != null)
|
||||
// todo: implement SpinnerFrequencyModulate
|
||||
spinningSample.Frequency.Value = 0.5f + Progress;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!SpmCounter.IsPresent && Disc.Tracking)
|
||||
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
|
||||
SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
||||
SpmCounter.SetRotation(RotationTracker.CumulativeRotation);
|
||||
|
||||
circle.Rotation = Disc.Rotation;
|
||||
Ticks.Rotation = Disc.Rotation;
|
||||
SpmCounter.SetRotation(Disc.CumulativeRotation);
|
||||
|
||||
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)));
|
||||
|
||||
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
|
||||
updateBonusScore();
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
private int wholeSpins;
|
||||
|
||||
private void updateBonusScore()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
if (ticks.Count == 0)
|
||||
return;
|
||||
|
||||
circleContainer.ScaleTo(Spinner.Scale * 0.3f);
|
||||
circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint);
|
||||
int spins = (int)(RotationTracker.CumulativeRotation / 360);
|
||||
|
||||
mainContainer
|
||||
.ScaleTo(0)
|
||||
.ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint)
|
||||
.Then()
|
||||
.ScaleTo(1, 500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void UpdateStateTransforms(ArmedState state)
|
||||
{
|
||||
base.UpdateStateTransforms(state);
|
||||
|
||||
var sequence = this.Delay(Spinner.Duration).FadeOut(160);
|
||||
|
||||
switch (state)
|
||||
if (spins < wholeSpins)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out);
|
||||
break;
|
||||
// rewinding, silently handle
|
||||
wholeSpins = spins;
|
||||
return;
|
||||
}
|
||||
|
||||
case ArmedState.Miss:
|
||||
sequence.ScaleTo(Scale * 0.8f, 320, Easing.In);
|
||||
break;
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class DefaultSpinnerDisc : CompositeDrawable
|
||||
{
|
||||
private DrawableSpinner drawableSpinner;
|
||||
|
||||
private Spinner spinner;
|
||||
|
||||
private const float idle_alpha = 0.2f;
|
||||
private const float tracking_alpha = 0.4f;
|
||||
|
||||
private Color4 normalColour;
|
||||
private Color4 completeColour;
|
||||
|
||||
private SpinnerTicks ticks;
|
||||
|
||||
private int wholeRotationCount;
|
||||
|
||||
private SpinnerFill fill;
|
||||
private Container mainContainer;
|
||||
private SpinnerCentreLayer centre;
|
||||
private SpinnerBackgroundLayer background;
|
||||
|
||||
public DefaultSpinnerDisc()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
// we are slightly bigger than our parent, to clip the top and bottom of the circle
|
||||
// this should probably be revisited when scaled spinners are a thing.
|
||||
Scale = new Vector2(1.3f);
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, DrawableHitObject drawableHitObject)
|
||||
{
|
||||
drawableSpinner = (DrawableSpinner)drawableHitObject;
|
||||
spinner = (Spinner)drawableSpinner.HitObject;
|
||||
|
||||
normalColour = colours.BlueDark;
|
||||
completeColour = colours.YellowLight;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
mainContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new SpinnerBackgroundLayer(),
|
||||
fill = new SpinnerFill
|
||||
{
|
||||
Alpha = idle_alpha,
|
||||
AccentColour = normalColour
|
||||
},
|
||||
ticks = new SpinnerTicks
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AccentColour = normalColour
|
||||
},
|
||||
}
|
||||
},
|
||||
centre = new SpinnerCentreLayer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
|
||||
drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (drawableSpinner.RotationTracker.Complete.Value)
|
||||
{
|
||||
if (checkNewRotationCount)
|
||||
{
|
||||
fill.FinishTransforms(false, nameof(Alpha));
|
||||
fill
|
||||
.FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
|
||||
.Then()
|
||||
.FadeTo(tracking_alpha, 250, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
|
||||
}
|
||||
|
||||
const float initial_scale = 0.2f;
|
||||
float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
|
||||
|
||||
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
|
||||
mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
|
||||
}
|
||||
|
||||
private void updateStateTransforms(ValueChangedEvent<ArmedState> state)
|
||||
{
|
||||
centre.ScaleTo(0);
|
||||
mainContainer.ScaleTo(0);
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
|
||||
{
|
||||
// constant ambient rotation to give the spinner "spinning" character.
|
||||
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
|
||||
|
||||
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
|
||||
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
|
||||
|
||||
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
|
||||
{
|
||||
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
|
||||
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
|
||||
updateComplete(state.NewValue == ArmedState.Hit, 0);
|
||||
|
||||
using (BeginDelayedSequence(spinner.Duration, true))
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
|
||||
this.RotateTo(mainContainer.Rotation + 180, 320);
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.ScaleTo(Scale * 0.8f, 320, Easing.In);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateComplete(bool complete, double duration)
|
||||
{
|
||||
var colour = complete ? completeColour : normalColour;
|
||||
|
||||
ticks.FadeAccent(colour.Darken(1), duration);
|
||||
fill.FadeAccent(colour.Darken(1), duration);
|
||||
|
||||
background.FadeAccent(colour, duration);
|
||||
centre.FadeAccent(colour, duration);
|
||||
}
|
||||
|
||||
private bool checkNewRotationCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360);
|
||||
|
||||
if (wholeRotationCount == rotations) return false;
|
||||
|
||||
wholeRotationCount = rotations;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows incremental bonus score achieved for a spinner.
|
||||
/// </summary>
|
||||
public class SpinnerBonusDisplay : CompositeDrawable
|
||||
{
|
||||
private readonly OsuSpriteText bonusCounter;
|
||||
|
||||
public SpinnerBonusDisplay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = bonusCounter = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Numeric.With(size: 24),
|
||||
Alpha = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private int displayedCount;
|
||||
|
||||
public void SetBonusCount(int count)
|
||||
{
|
||||
if (displayedCount == count)
|
||||
return;
|
||||
|
||||
displayedCount = count;
|
||||
bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}";
|
||||
bonusCounter.FadeOutFromOne(1500);
|
||||
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
public class SpinnerFill : CircularContainer, IHasAccentColour
|
||||
{
|
||||
protected Box Disc;
|
||||
public readonly Box Disc;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
@@ -31,11 +31,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
}
|
||||
}
|
||||
|
||||
public SpinnerBackground()
|
||||
public SpinnerFill()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Disc = new Box
|
||||
@@ -2,76 +2,33 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class SpinnerDisc : CircularContainer, IHasAccentColour
|
||||
public class SpinnerRotationTracker : CircularContainer
|
||||
{
|
||||
private readonly Spinner spinner;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => background.AccentColour;
|
||||
set => background.AccentColour = value;
|
||||
}
|
||||
|
||||
private readonly SpinnerBackground background;
|
||||
|
||||
private const float idle_alpha = 0.2f;
|
||||
private const float tracking_alpha = 0.4f;
|
||||
|
||||
public override bool IsPresent => true; // handle input when hidden
|
||||
|
||||
public SpinnerDisc(Spinner s)
|
||||
public SpinnerRotationTracker(Spinner s)
|
||||
{
|
||||
spinner = s;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new SpinnerBackground { Alpha = idle_alpha },
|
||||
};
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
private bool tracking;
|
||||
public bool Tracking { get; set; }
|
||||
|
||||
public bool Tracking
|
||||
{
|
||||
get => tracking;
|
||||
set
|
||||
{
|
||||
if (value == tracking) return;
|
||||
|
||||
tracking = value;
|
||||
|
||||
background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100);
|
||||
}
|
||||
}
|
||||
|
||||
private bool complete;
|
||||
|
||||
public bool Complete
|
||||
{
|
||||
get => complete;
|
||||
set
|
||||
{
|
||||
if (value == complete) return;
|
||||
|
||||
complete = value;
|
||||
|
||||
updateCompleteTick();
|
||||
}
|
||||
}
|
||||
public readonly BindableBool Complete = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// The total rotation performed on the spinner disc, disregarding the spin direction.
|
||||
@@ -84,7 +41,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
/// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
|
||||
/// this property will return the value of 720 (as opposed to 0 for <see cref="Drawable.Rotation"/>).
|
||||
/// </example>
|
||||
public float CumulativeRotation;
|
||||
public float CumulativeRotation { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
|
||||
/// </summary>
|
||||
public readonly BindableBool IsSpinning = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether currently in the correct time range to allow spinning.
|
||||
@@ -101,9 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
|
||||
private float lastAngle;
|
||||
private float currentRotation;
|
||||
private int completeTick;
|
||||
|
||||
private bool updateCompleteTick() => completeTick != (completeTick = (int)(CumulativeRotation / 360));
|
||||
|
||||
private bool rotationTransferred;
|
||||
|
||||
@@ -114,21 +73,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
|
||||
var delta = thisAngle - lastAngle;
|
||||
|
||||
if (tracking)
|
||||
Rotate(delta);
|
||||
if (Tracking)
|
||||
AddRotation(delta);
|
||||
|
||||
lastAngle = thisAngle;
|
||||
|
||||
if (Complete && updateCompleteTick())
|
||||
{
|
||||
background.FinishTransforms(false, nameof(Alpha));
|
||||
background
|
||||
.FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
|
||||
.Then()
|
||||
.FadeTo(tracking_alpha, 250, Easing.OutQuint);
|
||||
}
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
|
||||
|
||||
Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -138,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
/// Will be a no-op if not a valid time to spin.
|
||||
/// </remarks>
|
||||
/// <param name="angle">The delta angle.</param>
|
||||
public void Rotate(float angle)
|
||||
public void AddRotation(float angle)
|
||||
{
|
||||
if (!isSpinnableTime)
|
||||
return;
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class SpinnerBackgroundLayer : SpinnerFill
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, DrawableHitObject drawableHitObject)
|
||||
{
|
||||
Disc.Alpha = 0;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class SpinnerCentreLayer : CompositeDrawable, IHasAccentColour
|
||||
{
|
||||
private DrawableSpinner spinner;
|
||||
|
||||
private CirclePiece circle;
|
||||
private GlowPiece glow;
|
||||
private SpriteIcon symbol;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
spinner = (DrawableSpinner)drawableHitObject;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
glow = new GlowPiece(),
|
||||
circle = new CirclePiece
|
||||
{
|
||||
Position = Vector2.Zero,
|
||||
Anchor = Anchor.Centre,
|
||||
},
|
||||
new RingPiece(),
|
||||
symbol = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(48),
|
||||
Icon = FontAwesome.Solid.Asterisk,
|
||||
Shadow = false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, spinner.RotationTracker.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => accentColour;
|
||||
set
|
||||
{
|
||||
accentColour = value;
|
||||
|
||||
circle.Colour = accentColour;
|
||||
glow.Colour = accentColour;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,28 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SpinnerBonusTick : SpinnerTick
|
||||
{
|
||||
public new const int SCORE_PER_TICK = 50;
|
||||
|
||||
public SpinnerBonusTick()
|
||||
{
|
||||
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
|
||||
|
||||
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
|
||||
{
|
||||
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SpinnerTick : OsuHitObject
|
||||
{
|
||||
public const int SCORE_PER_TICK = 10;
|
||||
|
||||
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public class OsuSpinnerTickJudgement : OsuJudgement
|
||||
{
|
||||
public override bool AffectsCombo => false;
|
||||
|
||||
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
|
||||
|
||||
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,5 +17,6 @@ namespace osu.Game.Rulesets.Osu
|
||||
SliderFollowCircle,
|
||||
SliderBall,
|
||||
SliderBody,
|
||||
SpinnerBody
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy skinned spinner with two main spinning layers, one fixed overlay and one final spinning overlay.
|
||||
/// No background layer.
|
||||
/// </summary>
|
||||
public class LegacyNewStyleSpinner : CompositeDrawable
|
||||
{
|
||||
private Sprite discBottom;
|
||||
private Sprite discTop;
|
||||
private Sprite spinningMiddle;
|
||||
private Sprite fixedMiddle;
|
||||
|
||||
private DrawableSpinner drawableSpinner;
|
||||
|
||||
private const float final_scale = 0.625f;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource source, DrawableHitObject drawableObject)
|
||||
{
|
||||
drawableSpinner = (DrawableSpinner)drawableObject;
|
||||
|
||||
Scale = new Vector2(final_scale);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
discBottom = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-bottom")
|
||||
},
|
||||
discTop = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-top")
|
||||
},
|
||||
fixedMiddle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-middle")
|
||||
},
|
||||
spinningMiddle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-middle2")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
this.FadeOut();
|
||||
drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(ValueChangedEvent<ArmedState> state)
|
||||
{
|
||||
var spinner = (Spinner)drawableSpinner.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
|
||||
this.FadeInFromZero(spinner.TimePreempt / 2);
|
||||
|
||||
fixedMiddle.FadeColour(Color4.White);
|
||||
using (BeginAbsoluteSequence(spinner.StartTime, true))
|
||||
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation;
|
||||
discBottom.Rotation = discTop.Rotation / 3;
|
||||
|
||||
Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy skinned spinner with one main spinning layer and a background layer.
|
||||
/// </summary>
|
||||
public class LegacyOldStyleSpinner : CompositeDrawable
|
||||
{
|
||||
private DrawableSpinner drawableSpinner;
|
||||
private Sprite disc;
|
||||
private Container metre;
|
||||
|
||||
private const float background_y_offset = 20;
|
||||
|
||||
private const float sprite_scale = 1 / 1.6f;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource source, DrawableHitObject drawableObject)
|
||||
{
|
||||
drawableSpinner = (DrawableSpinner)drawableObject;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Texture = source.GetTexture("spinner-background"),
|
||||
Y = background_y_offset,
|
||||
Scale = new Vector2(sprite_scale)
|
||||
},
|
||||
disc = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-circle"),
|
||||
Scale = new Vector2(sprite_scale)
|
||||
},
|
||||
metre = new Container
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Y = background_y_offset,
|
||||
Masking = true,
|
||||
Child = new Sprite
|
||||
{
|
||||
Texture = source.GetTexture("spinner-metre"),
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
},
|
||||
Scale = new Vector2(0.625f)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Vector2 metreFinalSize;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
this.FadeOut();
|
||||
drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
|
||||
|
||||
metreFinalSize = metre.Size = metre.Child.Size;
|
||||
}
|
||||
|
||||
private void updateStateTransforms(ValueChangedEvent<ArmedState> state)
|
||||
{
|
||||
var spinner = drawableSpinner.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
|
||||
this.FadeInFromZero(spinner.TimePreempt / 2);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
disc.Rotation = drawableSpinner.RotationTracker.Rotation;
|
||||
metre.Height = getMetreHeight(drawableSpinner.Progress);
|
||||
}
|
||||
|
||||
private const int total_bars = 10;
|
||||
|
||||
private float getMetreHeight(float progress)
|
||||
{
|
||||
progress = Math.Min(99, progress * 100);
|
||||
|
||||
int barCount = (int)progress / 10;
|
||||
|
||||
// todo: add SpinnerNoBlink support
|
||||
if (RNG.NextBool(((int)progress % 10) / 10f))
|
||||
barCount++;
|
||||
|
||||
return (float)barCount / total_bars * metreFinalSize.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
case OsuSkinComponents.HitCircleText:
|
||||
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
|
||||
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0;
|
||||
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
|
||||
|
||||
return !hasFont(font)
|
||||
? null
|
||||
@@ -102,6 +102,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
Scale = new Vector2(0.8f),
|
||||
Spacing = new Vector2(-overlap, 0)
|
||||
};
|
||||
|
||||
case OsuSkinComponents.SpinnerBody:
|
||||
bool hasBackground = Source.GetTexture("spinner-background") != null;
|
||||
|
||||
if (Source.GetTexture("spinner-top") != null && !hasBackground)
|
||||
return new LegacyNewStyleSpinner();
|
||||
else if (hasBackground)
|
||||
return new LegacyOldStyleSpinner();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 1
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,1,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,0,0:0:0:0:
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -8,6 +8,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -36,6 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
private TaikoScoreProcessor scoreProcessor;
|
||||
|
||||
private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>();
|
||||
|
||||
private IEnumerable<DrawableTaikoMascot> animatedMascots =>
|
||||
mascots.Where(mascot => mascot.ChildrenOfType<TextureAnimation>().All(animation => animation.FrameCount > 0));
|
||||
|
||||
private IEnumerable<TaikoPlayfield> playfields => this.ChildrenOfType<TaikoPlayfield>();
|
||||
|
||||
[SetUp]
|
||||
@@ -72,11 +77,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
|
||||
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
|
||||
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
|
||||
AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear));
|
||||
AddAssert("skins with animations remain in clear state", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
|
||||
AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail));
|
||||
|
||||
AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
|
||||
AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear));
|
||||
AddAssert("skins with animations change to clear", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
|
||||
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
|
||||
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.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);
|
||||
}
|
||||
|
||||
@@ -186,10 +191,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
|
||||
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
|
||||
{
|
||||
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
|
||||
() => applyNewResult(judgementResult));
|
||||
TaikoMascotAnimationState[] mascotStates = null;
|
||||
|
||||
AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState));
|
||||
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
|
||||
() =>
|
||||
{
|
||||
applyNewResult(judgementResult);
|
||||
// store the states as soon as possible, so that the delay between steps doesn't incorrectly fail the test
|
||||
// due to not checking if the state changed quickly enough.
|
||||
Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
|
||||
});
|
||||
|
||||
AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState));
|
||||
}
|
||||
|
||||
private void applyNewResult(JudgementResult judgementResult)
|
||||
@@ -211,6 +224,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
}
|
||||
|
||||
private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state);
|
||||
private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state);
|
||||
private bool animatedMascotsIn(TaikoMascotAnimationState state) => animatedMascots.Any(d => d.State.Value == state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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.Reflection;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public class TestSceneTaikoHitObjectSamples : HitObjectSampleTest
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||
|
||||
protected override IResourceStore<byte[]> Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
|
||||
|
||||
[TestCase("taiko-normal-hitnormal")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
[TestCase("hitnormal")]
|
||||
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
|
||||
{
|
||||
SetupSkins(expectedSample, expectedSample);
|
||||
|
||||
CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(expectedSample);
|
||||
}
|
||||
|
||||
[TestCase("taiko-normal-hitnormal")]
|
||||
[TestCase("normal-hitnormal")]
|
||||
[TestCase("hitnormal")]
|
||||
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
|
||||
{
|
||||
SetupSkins(string.Empty, expectedSample);
|
||||
|
||||
CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertUserLookup(expectedSample);
|
||||
}
|
||||
|
||||
[TestCase("taiko-normal-hitnormal2")]
|
||||
[TestCase("normal-hitnormal2")]
|
||||
public void TestUserSkinLookupIgnoresSampleBank(string unwantedSample)
|
||||
{
|
||||
SetupSkins(string.Empty, unwantedSample);
|
||||
|
||||
CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertNoLookup(unwantedSample);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||