Merge branch 'master' into fix-keybind-clearing

This commit is contained in:
Dan Balasescu 2020-08-03 15:26:09 +09:00 committed by GitHub
commit 6511d6416b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
187 changed files with 4162 additions and 1413 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.731.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.714.1" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.730.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -8,8 +10,27 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public class Banana : Fruit public class Banana : Fruit
{ {
/// <summary>
/// Index of banana in current shower.
/// </summary>
public int BananaIndex;
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement(); 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;
}
} }
} }

View File

@ -30,15 +30,21 @@ namespace osu.Game.Rulesets.Catch.Objects
if (spacing <= 0) if (spacing <= 0)
return; return;
for (double i = StartTime; i <= EndTime; i += spacing) double time = StartTime;
int i = 0;
while (time <= EndTime)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
AddNested(new Banana AddNested(new Banana
{ {
Samples = Samples, StartTime = time,
StartTime = i BananaIndex = i,
}); });
time += spacing;
i++;
} }
} }

View File

@ -40,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1); 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() private Color4 getBananaColour()
{ {
switch (RNG.Next(0, 3)) switch (RNG.Next(0, 3))

View File

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

View File

@ -1,23 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
[TestFixture] [TestFixture]
public class TestSceneHitExplosion : ManiaSkinnableTestScene public class TestSceneHitExplosion : ManiaSkinnableTestScene
{ {
private readonly List<DrawablePool<PoolableHitExplosion>> hitExplosionPools = new List<DrawablePool<PoolableHitExplosion>>();
public TestSceneHitExplosion() public TestSceneHitExplosion()
{ {
int runcount = 0; int runcount = 0;
@ -29,28 +33,40 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (runcount % 15 > 12) if (runcount % 15 > 12)
return; 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), c.Add(hitExplosionPools[poolIndex].Get(e =>
_ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0) {
{ e.Apply(new JudgementResult(new HitObject(), runcount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
Anchor = Anchor.Centre,
Origin = Anchor.Centre, e.Anchor = Anchor.Centre;
})); e.Origin = Anchor.Centre;
}); }));
poolIndex++;
}
}, 100); }, 100);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1) SetContents(() =>
{ {
Anchor = Anchor.Centre, var pool = new DrawablePool<PoolableHitExplosion>(5);
Origin = Anchor.Centre, hitExplosionPools.Add(pool);
RelativePositionAxes = Axes.Y,
Y = -0.25f, return new ColumnTestContainer(0, ManiaAction.Key1)
Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT), {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Y,
Y = -0.25f,
Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
Child = pool
};
}); });
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,13 +6,15 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning namespace osu.Game.Rulesets.Mania.Skinning
{ {
public class LegacyHitExplosion : LegacyManiaColumnElement public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
{ {
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); 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; 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) explosion?.FadeInFromZero(80)
.Then().FadeOut(120); .Then().FadeOut(120);

View File

@ -9,9 +9,9 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>(); public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>();
public readonly ColumnHitObjectArea HitObjectArea; public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer; internal readonly Container TopLevelContainer;
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
public Container UnderlayElements => HitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[] 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 // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(), background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
@ -108,15 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return; return;
var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ => HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick))
{
RelativeSizeAxes = Axes.Both
};
HitObjectArea.Explosions.Add(explosion);
explosion.Delay(200).Expire(true);
} }
public bool OnPressed(ManiaAction action) public bool OnPressed(ManiaAction action)

View File

@ -8,6 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Utils; 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.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
@ -15,35 +17,36 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI 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; public override bool RemoveWhenNotAlive => true;
[Resolved]
private Column column { get; set; }
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly CircularContainer largeFaint; private CircularContainer largeFaint;
private readonly CircularContainer mainGlow1; private CircularContainer mainGlow1;
public DefaultHitExplosion(Color4 objectColour, bool isSmall = false) public DefaultHitExplosion()
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = DefaultNotePiece.NOTE_HEIGHT; Height = DefaultNotePiece.NOTE_HEIGHT;
}
// scale roughly in-line with visual appearance of notes [BackgroundDependencyLoader]
Scale = new Vector2(1f, 0.6f); private void load(IScrollingInfo scrollingInfo)
{
if (isSmall)
Scale *= 0.5f;
const float angle_variangle = 15; // should be less than 45 const float angle_variangle = 15; // should be less than 45
const float roundness = 80; const float roundness = 80;
const float initial_height = 10; 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[] InternalChildren = new Drawable[]
{ {
@ -54,12 +57,12 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
// we want our size to be very small so the glow dominates it. // 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, Blending = BlendingParameters.Additive,
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, 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, Roundness = 160,
Radius = 200, Radius = 200,
}, },
@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.UI
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, 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, Roundness = 20,
Radius = 50, Radius = 50,
}, },
@ -114,30 +117,11 @@ namespace osu.Game.Rulesets.Mania.UI
}, },
} }
}; };
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true); 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) private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{ {
if (direction.NewValue == ScrollingDirection.Up) if (direction.NewValue == ScrollingDirection.Up)
@ -151,5 +135,29 @@ namespace osu.Game.Rulesets.Mania.UI
Y = -DefaultNotePiece.NOTE_HEIGHT / 2; 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);
}
} }
} }

View File

@ -15,6 +15,10 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
} }
public DrawableManiaJudgement()
{
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {

View File

@ -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);
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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();
}
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
@ -33,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IReadOnlyList<Column> Columns => columnFlow.Children; public IReadOnlyList<Column> Columns => columnFlow.Children;
private readonly FillFlowContainer<Column> columnFlow; private readonly FillFlowContainer<Column> columnFlow;
public Container<DrawableManiaJudgement> Judgements => judgements;
private readonly JudgementContainer<DrawableManiaJudgement> judgements; private readonly JudgementContainer<DrawableManiaJudgement> judgements;
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
private readonly Drawable barLineContainer; private readonly Drawable barLineContainer;
private readonly Container topLevelContainer; private readonly Container topLevelContainer;
@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
new Container new Container
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -208,12 +210,14 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value) if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return; return;
judgements.Clear(); judgements.Clear(false);
judgements.Add(new DrawableManiaJudgement(result, judgedObject) judgements.Add(judgementPool.Get(j =>
{ {
Anchor = Anchor.Centre, j.Apply(result, judgedObject);
Origin = Anchor.Centre,
}); j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
}));
} }
protected override void Update() protected override void Update()

View File

@ -1,12 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
public abstract class OsuSkinnableTestScene : SkinnableTestScene 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(); protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

View File

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

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -26,19 +25,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture] [TestFixture]
public class TestSceneSlider : OsuSkinnableTestScene 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; private int depthIndex;
public TestSceneSlider() public TestSceneSlider()

View File

@ -4,37 +4,30 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
[TestFixture] [TestFixture]
public class TestSceneSpinner : OsuTestScene public class TestSceneSpinner : OsuSkinnableTestScene
{ {
private readonly Container content;
protected override Container<Drawable> Content => content;
private int depthIndex; private int depthIndex;
public TestSceneSpinner() public TestSceneSpinner()
{ {
base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); AddStep("Miss Big", () => SetContents(() => testSingle(2)));
AddStep("Miss Medium", () => SetContents(() => testSingle(5)));
AddStep("Miss Big", () => testSingle(2)); AddStep("Miss Small", () => SetContents(() => testSingle(7)));
AddStep("Miss Medium", () => testSingle(5)); AddStep("Hit Big", () => SetContents(() => testSingle(2, true)));
AddStep("Miss Small", () => testSingle(7)); AddStep("Hit Medium", () => SetContents(() => testSingle(5, true)));
AddStep("Hit Big", () => testSingle(2, true)); AddStep("Hit Small", () => SetContents(() => testSingle(7, true)));
AddStep("Hit Medium", () => testSingle(5, true));
AddStep("Hit Small", () => 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 + 2000, EndTime = Time.Current + 5000 }; var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
@ -49,12 +42,12 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObjects(new[] { drawable });
Add(drawable); return drawable;
} }
private class TestDrawableSpinner : DrawableSpinner private class TestDrawableSpinner : DrawableSpinner
{ {
private bool auto; private readonly bool auto;
public TestDrawableSpinner(Spinner s, bool auto) public TestDrawableSpinner(Spinner s, bool auto)
: base(s) : base(s)
@ -62,16 +55,11 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto; 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) base.Update();
{ if (auto)
// force completion only once to not break human interaction RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3));
Disc.CumulativeRotation = Spinner.SpinsRequired * 360;
auto = false;
}
base.CheckForResult(userTriggered, timeOffset);
} }
} }
} }

View File

@ -1,26 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Utils; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@ -34,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool Autoplay => true; protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ {
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
@ -56,39 +61,60 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test] [Test]
public void TestSpinnerRewindingRotation() public void TestSpinnerRewindingRotation()
{ {
double trackerRotationTolerance = 0;
addSeekStep(5000); addSeekStep(5000);
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100)); AddStep("calculate rotation tolerance", () =>
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100)); {
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
});
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); addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100)); AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100)); AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
} }
[Test] [Test]
public void TestSpinnerMiddleRewindingRotation() public void TestSpinnerMiddleRewindingRotation()
{ {
double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0; double finalCumulativeTrackerRotation = 0;
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000); addSeekStep(5000);
AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation); AddStep("retrieve disc rotation", () =>
AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.CumulativeRotation); {
AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation); finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
});
AddStep("retrieve spinner symbol rotation", () =>
{
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation);
addSeekStep(2500); addSeekStep(2500);
AddUntilStep("disc rotation rewound", AddAssert("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. // 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)); // due to the exponential damping applied we're allowing a larger margin of error of about 10%
AddUntilStep("symbol rotation rewound", // (5% relative to the final rotation value, but we're half-way through the spin).
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100)); () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance));
AddAssert("symbol rotation rewound",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000); addSeekStep(5000);
AddAssert("is disc rotation almost same", AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation, 100)); () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same", AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100)); () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
AddAssert("is disc rotation absolute almost same", AddAssert("is cumulative rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, finalAbsoluteDiscRotation, 100)); () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation, 100));
} }
[Test] [Test]
@ -111,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(5000); 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); AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
} }
@ -129,18 +155,44 @@ namespace osu.Game.Rulesets.Osu.Tests
.ToList() .ToList()
}; };
[Test]
public void TestSpinnerNormalBonusRewinding()
{
addSeekStep(1000);
AddAssert("player score matching expected bonus score", () =>
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.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] [Test]
public void TestSpinPerMinuteOnRewind() public void TestSpinPerMinuteOnRewind()
{ {
double estimatedSpm = 0; double estimatedSpm = 0;
addSeekStep(2500); addSeekStep(1000);
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
addSeekStep(5000); addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
addSeekStep(2500); addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
} }
@ -160,12 +212,17 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
EndTime = 6000, EndTime = 6000,
}, },
// placeholder object to avoid hitting the results screen
new HitCircle
{
StartTime = 99999,
}
} }
}; };
private class ScoreExposedPlayer : TestPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public ScoreExposedPlayer()
: base(false, false)
{
}
}
} }
} }

View File

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

View File

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

View File

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

View File

@ -82,9 +82,7 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableSpinner spinner: case DrawableSpinner spinner:
// hide elements we don't care about. // hide elements we don't care about.
spinner.Disc.Hide(); // todo: hide background
spinner.Ticks.Hide();
spinner.Background.Hide();
using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true)) using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true))
spinner.FadeOut(fadeOutDuration); spinner.FadeOut(fadeOutDuration);

View File

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

View File

@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
var spinner = (DrawableSpinner)drawable; var spinner = (DrawableSpinner)drawable;
spinner.Disc.Tracking = true; spinner.RotationTracker.Tracking = true;
spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
} }
} }
} }

View File

@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
var h = drawableOsu.HitObject; var h = drawableOsu.HitObject;
//todo: expose and hide spinner background somehow
switch (drawable) switch (drawable)
{ {
case DrawableHitCircle circle: case DrawableHitCircle circle:
@ -56,11 +58,6 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.Body.OnSkinChanged += () => applySliderState(slider); slider.Body.OnSkinChanged += () => applySliderState(slider);
applySliderState(slider); applySliderState(slider);
break; break;
case DrawableSpinner spinner:
spinner.Disc.Hide();
spinner.Background.Hide();
break;
} }
} }

View File

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

View File

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

View File

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

View File

@ -3,20 +3,18 @@
using System; using System;
using System.Linq; 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.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics;
using osu.Framework.Utils; 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.Rulesets.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -24,27 +22,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
protected readonly Spinner Spinner; protected readonly Spinner Spinner;
public readonly SpinnerDisc Disc; private readonly Container<DrawableSpinnerTick> ticks;
public readonly SpinnerTicks Ticks;
public readonly SpinnerRotationTracker RotationTracker;
public readonly SpinnerSpmCounter SpmCounter; public readonly SpinnerSpmCounter SpmCounter;
private readonly SpinnerBonusDisplay bonusDisplay;
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 IBindable<Vector2> positionBindable = new Bindable<Vector2>(); private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private Color4 normalColour;
private Color4 completeColour;
public DrawableSpinner(Spinner s) public DrawableSpinner(Spinner s)
: base(s) : base(s)
{ {
@ -53,65 +38,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
// we are slightly bigger than our parent, to clip the top and bottom of the circle
Height = 1.3f;
Spinner = s; Spinner = s;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
circleContainer = new Container ticks = new Container<DrawableSpinnerTick>(),
{ new AspectContainer
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
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Children = new[] Children = new Drawable[]
{ {
Background = new SpinnerBackground new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()),
{ RotationTracker = new SpinnerRotationTracker(Spinner)
Disc =
{
Alpha = 0f,
},
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,
},
} }
}, },
SpmCounter = new SpinnerSpmCounter SpmCounter = new SpinnerSpmCounter
@ -120,42 +60,136 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre, Origin = Anchor.Centre,
Y = 120, Y = 120,
Alpha = 0 Alpha = 0
},
bonusDisplay = new SpinnerBonusDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -120,
} }
}; };
} }
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] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
normalColour = baseColour;
completeColour = colours.YellowLight;
Background.AccentColour = normalColour;
Ticks.AccentColour = normalColour;
Disc.AccentColour = fillColour;
circle.Colour = colours.BlueDark;
glow.Colour = colours.BlueDark;
positionBindable.BindValueChanged(pos => Position = pos.NewValue); positionBindable.BindValueChanged(pos => Position = pos.NewValue);
positionBindable.BindTo(HitObject.PositionBindable); 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) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (Time.Current < HitObject.StartTime) return; if (Time.Current < HitObject.StartTime) return;
if (Progress >= 1 && !Disc.Complete) RotationTracker.Complete.Value = Progress >= 1;
{
Disc.Complete = true;
transformFillColour(completeColour, 200);
}
if (userTriggered || Time.Current < Spinner.EndTime) if (userTriggered || Time.Current < Spinner.EndTime)
return; return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
foreach (var tick in ticks.Where(t => !t.IsHit))
tick.TriggerResult(false);
ApplyResult(r => ApplyResult(r =>
{ {
if (Progress >= 1) if (Progress >= 1)
@ -172,85 +206,56 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (HandleUserInput) 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() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
if (!SpmCounter.IsPresent && Disc.Tracking) if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn); SpmCounter.FadeIn(HitObject.TimeFadeIn);
SpmCounter.SetRotation(RotationTracker.CumulativeRotation);
circle.Rotation = Disc.Rotation; updateBonusScore();
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));
} }
protected override void UpdateInitialTransforms() private int wholeSpins;
private void updateBonusScore()
{ {
base.UpdateInitialTransforms(); if (ticks.Count == 0)
return;
circleContainer.ScaleTo(0); int spins = (int)(RotationTracker.CumulativeRotation / 360);
mainContainer.ScaleTo(0);
using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) if (spins < wholeSpins)
{ {
float phaseOneScale = Spinner.Scale * 0.7f; // rewinding, silently handle
wholeSpins = spins;
circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint); return;
mainContainer
.ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint)
.RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration);
using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
{
circleContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint);
mainContainer.ScaleTo(1, 400, Easing.OutQuint);
}
} }
}
protected override void UpdateStateTransforms(ArmedState state) while (wholeSpins != spins)
{
base.UpdateStateTransforms(state);
using (BeginDelayedSequence(Spinner.Duration, true))
{ {
this.FadeOut(160); var tick = ticks.FirstOrDefault(t => !t.IsHit);
switch (state) // tick may be null if we've hit the spin limit.
if (tick != null)
{ {
case ArmedState.Hit: tick.TriggerResult(true);
transformFillColour(completeColour, 0); if (tick is DrawableSpinnerBonusTick)
this.ScaleTo(Scale * 1.2f, 320, Easing.Out); bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired);
mainContainer.RotateTo(mainContainer.Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(Scale * 0.8f, 320, Easing.In);
break;
} }
wholeSpins++;
} }
} }
private void transformFillColour(Colour4 colour, double duration)
{
Disc.FadeAccent(colour, duration);
Background.FadeAccent(colour.Darken(1), duration);
Ticks.FadeAccent(colour, duration);
circle.FadeColour(colour, duration);
glow.FadeColour(colour, duration);
}
} }
} }

View File

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

View File

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

View File

@ -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;
}
}
}
}

View File

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

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
/// <summary>
/// Shows incremental bonus score achieved for a spinner.
/// </summary>
public class SpinnerBonusDisplay : CompositeDrawable
{
private readonly OsuSpriteText bonusCounter;
public SpinnerBonusDisplay()
{
AutoSizeAxes = Axes.Both;
InternalChild = bonusCounter = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 24),
Alpha = 0,
};
}
private int displayedCount;
public void SetBonusCount(int count)
{
if (displayedCount == count)
return;
displayedCount = count;
bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}";
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
}
}
}

View File

@ -10,7 +10,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerBackground : CircularContainer, IHasAccentColour public class SpinnerFill : CircularContainer, IHasAccentColour
{ {
public readonly Box Disc; public readonly Box Disc;
@ -31,11 +31,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
} }
} }
public SpinnerBackground() public SpinnerFill()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Masking = true; Masking = true;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Children = new Drawable[] Children = new Drawable[]
{ {
Disc = new Box Disc = new Box

View File

@ -2,76 +2,33 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerDisc : CircularContainer, IHasAccentColour public class SpinnerRotationTracker : CircularContainer
{ {
private readonly Spinner spinner; 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 override bool IsPresent => true; // handle input when hidden
public SpinnerDisc(Spinner s) public SpinnerRotationTracker(Spinner s)
{ {
spinner = s; spinner = s;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Children = new Drawable[]
{
background = new SpinnerBackground { Alpha = idle_alpha },
};
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private bool tracking; public bool Tracking { get; set; }
public bool Tracking public readonly BindableBool Complete = new BindableBool();
{
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();
}
}
/// <summary> /// <summary>
/// The total rotation performed on the spinner disc, disregarding the spin direction. /// 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, /// 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"/>). /// this property will return the value of 720 (as opposed to 0 for <see cref="Drawable.Rotation"/>).
/// </example> /// </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> /// <summary>
/// Whether currently in the correct time range to allow spinning. /// 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 lastAngle;
private float currentRotation; private float currentRotation;
private int completeTick;
private bool updateCompleteTick() => completeTick != (completeTick = (int)(CumulativeRotation / 360));
private bool rotationTransferred; private bool rotationTransferred;
@ -114,21 +73,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var delta = thisAngle - lastAngle; var delta = thisAngle - lastAngle;
if (tracking) if (Tracking)
Rotate(delta); AddRotation(delta);
lastAngle = thisAngle; lastAngle = thisAngle;
if (Complete && updateCompleteTick()) IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
{
background.FinishTransforms(false, nameof(Alpha));
background
.FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
.Then()
.FadeTo(tracking_alpha, 250, Easing.OutQuint);
}
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> /// <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. /// Will be a no-op if not a valid time to spin.
/// </remarks> /// </remarks>
/// <param name="angle">The delta angle.</param> /// <param name="angle">The delta angle.</param>
public void Rotate(float angle) public void AddRotation(float angle)
{ {
if (!isSpinnableTime) if (!isSpinnableTime)
return; return;

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

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

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerBonusTick : SpinnerTick
{
public new const int SCORE_PER_TICK = 50;
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
}
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
public const int SCORE_PER_TICK = 10;
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public class OsuSpinnerTickJudgement : OsuJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
}
}
}

View File

@ -17,5 +17,6 @@ namespace osu.Game.Rulesets.Osu
SliderFollowCircle, SliderFollowCircle,
SliderBall, SliderBall,
SliderBody, SliderBody,
SpinnerBody
} }
} }

View File

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

View File

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

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

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

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
case OsuSkinComponents.HitCircleText: case OsuSkinComponents.HitCircleText:
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0; var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
return !hasFont(font) return !hasFont(font)
? null ? null
@ -102,6 +102,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
Scale = new Vector2(0.8f), Scale = new Vector2(0.8f),
Spacing = new Vector2(-overlap, 0) 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; return null;

View File

@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

@ -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);
}
}
}

View File

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

View File

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

View File

@ -1,52 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Audio
{
public class DrumSampleMapping
{
private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
public readonly List<SkinnableSound> Sounds = new List<SkinnableSound>();
public DrumSampleMapping(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
IEnumerable<SampleControlPoint> samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
foreach (var s in samplePoints)
{
var centre = s.GetSampleInfo();
var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP);
mappings[s.Time] = new DrumSample
{
Centre = addSound(centre),
Rim = addSound(rim)
};
}
}
private SkinnableSound addSound(HitSampleInfo hitSampleInfo)
{
var drawable = new SkinnableSound(hitSampleInfo);
Sounds.Add(drawable);
return drawable;
}
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
public class DrumSample
{
public SkinnableSound Centre;
public SkinnableSound Rim;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -67,9 +67,11 @@ namespace osu.Game.Tests.Gameplay
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin: /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
/// normal-hitnormal2 /// normal-hitnormal2
/// normal-hitnormal /// normal-hitnormal
/// hitnormal
/// </summary> /// </summary>
[TestCase("normal-hitnormal2")] [TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")] [TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample) public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{ {
SetupSkins(expectedSample, expectedSample); SetupSkins(expectedSample, expectedSample);
@ -80,12 +82,13 @@ namespace osu.Game.Tests.Gameplay
} }
/// <summary> /// <summary>
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample: /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin
/// normal-hitnormal2 /// (ignoring the custom sample set index) when the beatmap skin does not contain the sample:
/// normal-hitnormal /// normal-hitnormal
/// hitnormal
/// </summary> /// </summary>
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")] [TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
{ {
SetupSkins(string.Empty, expectedSample); SetupSkins(string.Empty, expectedSample);
@ -95,6 +98,23 @@ namespace osu.Game.Tests.Gameplay
AssertUserLookup(expectedSample); AssertUserLookup(expectedSample);
} }
/// <summary>
/// Tests that a hitobject which provides a custom sample set of 2 does not retrieve a normal-hitnormal2 sample from the user skin
/// if the beatmap skin does not contain the sample.
/// User skins in stable ignore the custom sample set index when performing lookups.
/// </summary>
[Test]
public void TestUserSkinLookupIgnoresSampleBank()
{
const string unwanted_sample = "normal-hitnormal2";
SetupSkins(string.Empty, unwanted_sample);
CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
AssertNoLookup(unwanted_sample);
}
/// <summary> /// <summary>
/// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin. /// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
/// </summary> /// </summary>
@ -145,6 +165,7 @@ namespace osu.Game.Tests.Gameplay
/// </summary> /// </summary>
[TestCase("normal-hitnormal2")] [TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")] [TestCase("normal-hitnormal")]
[TestCase("hitnormal")]
public void TestControlPointCustomSampleFromBeatmap(string sampleName) public void TestControlPointCustomSampleFromBeatmap(string sampleName)
{ {
SetupSkins(sampleName, sampleName); SetupSkins(sampleName, sampleName);
@ -178,7 +199,7 @@ namespace osu.Game.Tests.Gameplay
string[] expectedSamples = string[] expectedSamples =
{ {
"normal-hitnormal2", "normal-hitnormal2",
"normal-hitwhistle2" "normal-hitwhistle" // user skin lookups ignore custom sample set index
}; };
SetupSkins(expectedSamples[0], expectedSamples[1]); SetupSkins(expectedSamples[0], expectedSamples[1]);

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -12,6 +20,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Multi.Ranking;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Users; using osu.Game.Users;
@ -19,43 +28,134 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneTimeshiftResultsScreen : ScreenTestScene public class TestSceneTimeshiftResultsScreen : ScreenTestScene
{ {
private bool roomsReceived; private const int scores_per_result = 10;
private TestResultsScreen resultsScreen;
private int currentScoreId;
private bool requestComplete;
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
{ {
roomsReceived = false; currentScoreId = 0;
requestComplete = false;
bindHandler(); bindHandler();
}); });
[Test] [Test]
public void TestShowResultsWithScore() public void TestShowWithUserScore()
{ {
createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo)); ScoreInfo userScore = null;
AddWaitStep("wait for display", 5);
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
bindHandler(userScore: userScore);
});
createResults(() => userScore);
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
} }
[Test] [Test]
public void TestShowResultsNullScore() public void TestShowNullUserScore()
{ {
createResults(null); createResults();
AddWaitStep("wait for display", 5); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
} }
[Test] [Test]
public void TestShowResultsNullScoreWithDelay() public void TestShowUserScoreWithDelay()
{
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
bindHandler(3000, userScore);
});
createResults(() => userScore);
waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
[Test]
public void TestShowNullUserScoreWithDelay()
{ {
AddStep("bind delayed handler", () => bindHandler(3000)); AddStep("bind delayed handler", () => bindHandler(3000));
createResults(null);
AddUntilStep("wait for rooms to be received", () => roomsReceived); createResults();
AddWaitStep("wait for display", 5); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
} }
private void createResults(ScoreInfo score) [Test]
public void TestFetchWhenScrolledToTheRight()
{
createResults();
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(3000));
for (int i = 0; i < 2; i++)
{
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
}
}
[Test]
public void TestFetchWhenScrolledToTheLeft()
{
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
bindHandler(userScore: userScore);
});
createResults(() => userScore);
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(3000));
for (int i = 0; i < 2; i++)
{
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
}
}
private void createResults(Func<ScoreInfo> getScore = null)
{ {
AddStep("load results", () => AddStep("load results", () =>
{ {
LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem
{ {
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo } Ruleset = { Value = new OsuRuleset().RulesetInfo }
@ -63,62 +163,214 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
} }
private void bindHandler(double delay = 0) private void waitForDisplay()
{ {
var roomScores = new List<RoomScore>(); AddUntilStep("wait for request to complete", () => requestComplete);
AddWaitStep("wait for display", 5);
}
for (int i = 0; i < 10; i++) private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
{
requestComplete = false;
if (failRequests)
{ {
roomScores.Add(new RoomScore triggerFail(request, delay);
return;
}
switch (request)
{
case ShowPlaylistUserScoreRequest s:
if (userScore == null)
triggerFail(s, delay);
else
triggerSuccess(s, createUserResponse(userScore), delay);
break;
case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i), delay);
break;
}
};
private void triggerSuccess<T>(APIRequest<T> req, T result, double delay)
where T : class
{
if (delay == 0)
success();
else
{
Task.Run(async () =>
{ {
ID = i, await Task.Delay(TimeSpan.FromMilliseconds(delay));
Accuracy = 0.9 - 0.01 * i, Schedule(success);
EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)), });
}
void success()
{
requestComplete = true;
req.TriggerSuccess(result);
}
}
private void triggerFail(APIRequest req, double delay)
{
if (delay == 0)
fail();
else
{
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(delay));
Schedule(fail);
});
}
void fail()
{
requestComplete = true;
req.TriggerFailure(new WebException("Failed."));
}
}
private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
{
var multiplayerUserScore = new MultiplayerScore
{
ID = (int)(userScore.OnlineScoreID ?? currentScoreId++),
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = userScore.Passed,
Rank = userScore.Rank,
Position = 200,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore,
User = userScore.User,
Statistics = userScore.Statistics,
ScoresAround = new MultiplayerScoresAround
{
Higher = new MultiplayerScores(),
Lower = new MultiplayerScores()
}
};
for (int i = 1; i <= scores_per_result; i++)
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = true, Passed = true,
Rank = ScoreRank.B, Rank = userScore.Rank,
MaxCombo = 999, MaxCombo = userScore.MaxCombo,
TotalScore = 999999 - i * 1000, TotalScore = userScore.TotalScore - i,
User = new User User = new User
{ {
Id = 2, Id = 2,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
Statistics = Statistics = userScore.Statistics
});
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = true,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore + i,
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics = userScore.Statistics
});
}
addCursor(multiplayerUserScore.ScoresAround.Lower);
addCursor(multiplayerUserScore.ScoresAround.Higher);
return multiplayerUserScore;
}
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
{
var result = new IndexedMultiplayerScores();
long startTotalScore = req.Cursor?.Properties["total_score"].ToObject<long>() ?? 1000000;
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
for (int i = 1; i <= scores_per_result; i++)
{
result.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
Accuracy = 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.X,
MaxCombo = 1000,
TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics = new Dictionary<HitResult, int>
{ {
{ HitResult.Miss, 1 }, { HitResult.Miss, 1 },
{ HitResult.Meh, 50 }, { HitResult.Meh, 50 },
{ HitResult.Good, 100 }, { HitResult.Good, 100 },
{ HitResult.Great, 300 }, { HitResult.Great, 300 }
} }
}); });
} }
((DummyAPIAccess)API).HandleRequest = request => addCursor(result);
return result;
}
private void addCursor(MultiplayerScores scores)
{
scores.Cursor = new Cursor
{ {
switch (request) Properties = new Dictionary<string, JToken>
{ {
case GetRoomPlaylistScoresRequest r: { "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) },
if (delay == 0) { "score_id", JToken.FromObject(scores.Scores[^1].ID) },
success(); }
else };
{
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(delay));
Schedule(success);
});
}
void success() scores.Params = new IndexScoresParams
{ {
r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores }); Properties = new Dictionary<string, JToken>
roomsReceived = true; {
} { "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
break;
} }
}; };
} }
private class TestResultsScreen : TimeshiftResultsScreen
{
public new LoadingSpinner LeftSpinner => base.LeftSpinner;
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, roomId, playlistItem, allowRetry)
{
}
}
} }
} }

View File

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

View File

@ -4,19 +4,22 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Graphics; using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneShowMoreButton : OsuTestScene public class TestSceneShowMoreButton : OsuTestScene
{ {
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
public TestSceneShowMoreButton() public TestSceneShowMoreButton()
{ {
TestButton button = null; ShowMoreButton button = null;
int fireCount = 0; int fireCount = 0;
Add(button = new TestButton Add(button = new ShowMoreButton
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -46,16 +49,5 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("action fired twice", () => fireCount == 2); AddAssert("action fired twice", () => fireCount == 2);
AddAssert("is in loading state", () => button.IsLoading); AddAssert("is in loading state", () => button.IsLoading);
} }
private class TestButton : ShowMoreButton
{
[BackgroundDependencyLoader]
private void load(OsuColour colors)
{
IdleColour = colors.YellowDark;
HoverColour = colors.Yellow;
ChevronIconColour = colors.Red;
}
}
} }
} }

View File

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

View File

@ -11,14 +11,14 @@ using osu.Game.Graphics.Sprites;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
[TestFixture] [TestFixture]
public class TestSceneHueAnimation : OsuTestScene public class TestSceneLogoAnimation : OsuTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LargeTextureStore textures) private void load(LargeTextureStore textures)
{ {
HueAnimation anim2; LogoAnimation anim2;
Add(anim2 = new HueAnimation Add(anim2 = new LogoAnimation
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
@ -26,9 +26,9 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour = Colour4.White, Colour = Colour4.White,
}); });
HueAnimation anim; LogoAnimation anim;
Add(anim = new HueAnimation Add(anim = new LogoAnimation
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,

View File

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

View File

@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{ {
} }
private class SettingsRoundDropdown : LadderSettingsDropdown<TournamentRound> private class SettingsRoundDropdown : SettingsDropdown<TournamentRound>
{ {
public SettingsRoundDropdown(BindableList<TournamentRound> rounds) public SettingsRoundDropdown(BindableList<TournamentRound> rounds)
{ {

View File

@ -1,26 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Tournament.Screens.Ladder.Components
{
public class LadderSettingsDropdown<T> : SettingsDropdown<T>
{
protected override OsuDropdown<T> CreateDropdown() => new DropdownControl();
private new class DropdownControl : SettingsDropdown<T>.DropdownControl
{
protected override DropdownMenu CreateMenu() => new Menu();
private new class Menu : OsuDropdownMenu
{
public Menu()
{
MaxHeight = 200;
}
}
}
}
}

View File

@ -6,11 +6,12 @@ using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components namespace osu.Game.Tournament.Screens.Ladder.Components
{ {
public class SettingsTeamDropdown : LadderSettingsDropdown<TournamentTeam> public class SettingsTeamDropdown : SettingsDropdown<TournamentTeam>
{ {
public SettingsTeamDropdown(BindableList<TournamentTeam> teams) public SettingsTeamDropdown(BindableList<TournamentTeam> teams)
{ {

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