mirror of
https://github.com/osukey/osukey.git
synced 2025-05-29 17:37:23 +09:00
Merge branch 'master' into grey-logo-animation
This commit is contained in:
commit
c1b9b847ec
@ -52,6 +52,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.722.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.723.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -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)
|
||||||
|
41
osu.Desktop/Windows/GameplayWinKeyBlocker.cs
Normal file
41
osu.Desktop/Windows/GameplayWinKeyBlocker.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
80
osu.Desktop/Windows/WindowsKey.cs
Normal file
80
osu.Desktop/Windows/WindowsKey.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
{
|
{
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osuTK;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Game.Replays;
|
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
|
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
protected override bool Autoplay => true;
|
protected override bool Autoplay => true;
|
||||||
|
|
||||||
|
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
|
||||||
|
|
||||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||||
{
|
{
|
||||||
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
@ -129,18 +133,44 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpinnerNormalBonusRewinding()
|
||||||
|
{
|
||||||
|
addSeekStep(1000);
|
||||||
|
|
||||||
|
AddAssert("player score matching expected bonus score", () =>
|
||||||
|
{
|
||||||
|
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||||
|
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||||
|
return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
addSeekStep(0);
|
||||||
|
|
||||||
|
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpinnerCompleteBonusRewinding()
|
||||||
|
{
|
||||||
|
addSeekStep(2500);
|
||||||
|
addSeekStep(0);
|
||||||
|
|
||||||
|
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSpinPerMinuteOnRewind()
|
public void TestSpinPerMinuteOnRewind()
|
||||||
{
|
{
|
||||||
double estimatedSpm = 0;
|
double estimatedSpm = 0;
|
||||||
|
|
||||||
addSeekStep(2500);
|
addSeekStep(1000);
|
||||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
||||||
|
|
||||||
addSeekStep(5000);
|
addSeekStep(2000);
|
||||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
||||||
|
|
||||||
addSeekStep(2500);
|
addSeekStep(1000);
|
||||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,12 +190,17 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
Position = new Vector2(256, 192),
|
Position = new Vector2(256, 192),
|
||||||
EndTime = 6000,
|
EndTime = 6000,
|
||||||
},
|
},
|
||||||
// placeholder object to avoid hitting the results screen
|
|
||||||
new HitCircle
|
|
||||||
{
|
|
||||||
StartTime = 99999,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private class ScoreExposedPlayer : TestPlayer
|
||||||
|
{
|
||||||
|
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||||
|
|
||||||
|
public ScoreExposedPlayer()
|
||||||
|
: base(false, false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
@ -24,9 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
protected readonly Spinner Spinner;
|
protected readonly Spinner Spinner;
|
||||||
|
|
||||||
|
private readonly Container<DrawableSpinnerTick> ticks;
|
||||||
|
|
||||||
public readonly SpinnerDisc Disc;
|
public readonly SpinnerDisc Disc;
|
||||||
public readonly SpinnerTicks Ticks;
|
public readonly SpinnerTicks Ticks;
|
||||||
public readonly SpinnerSpmCounter SpmCounter;
|
public readonly SpinnerSpmCounter SpmCounter;
|
||||||
|
private readonly SpinnerBonusDisplay bonusDisplay;
|
||||||
|
|
||||||
private readonly Container mainContainer;
|
private readonly Container mainContainer;
|
||||||
|
|
||||||
@ -60,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
|
ticks = new Container<DrawableSpinnerTick>(),
|
||||||
circleContainer = new Container
|
circleContainer = new Container
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
@ -120,10 +125,48 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Y = 120,
|
Y = 120,
|
||||||
Alpha = 0
|
Alpha = 0
|
||||||
|
},
|
||||||
|
bonusDisplay = new SpinnerBonusDisplay
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Y = -120,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
base.AddNestedHitObject(hitObject);
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case DrawableSpinnerTick tick:
|
||||||
|
ticks.Add(tick);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ClearNestedHitObjects()
|
||||||
|
{
|
||||||
|
base.ClearNestedHitObjects();
|
||||||
|
ticks.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||||
|
{
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case SpinnerBonusTick bonusTick:
|
||||||
|
return new DrawableSpinnerBonusTick(bonusTick);
|
||||||
|
|
||||||
|
case SpinnerTick tick:
|
||||||
|
return new DrawableSpinnerTick(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.CreateNestedHitObject(hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
@ -156,6 +199,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
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)
|
||||||
@ -185,8 +232,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
circle.Rotation = Disc.Rotation;
|
circle.Rotation = Disc.Rotation;
|
||||||
Ticks.Rotation = Disc.Rotation;
|
Ticks.Rotation = Disc.Rotation;
|
||||||
|
|
||||||
SpmCounter.SetRotation(Disc.CumulativeRotation);
|
SpmCounter.SetRotation(Disc.CumulativeRotation);
|
||||||
|
|
||||||
|
updateBonusScore();
|
||||||
|
|
||||||
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
|
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
|
||||||
float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
|
float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
|
||||||
Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
|
Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
|
||||||
@ -194,6 +244,38 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
|
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int wholeSpins;
|
||||||
|
|
||||||
|
private void updateBonusScore()
|
||||||
|
{
|
||||||
|
if (ticks.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int spins = (int)(Disc.CumulativeRotation / 360);
|
||||||
|
|
||||||
|
if (spins < wholeSpins)
|
||||||
|
{
|
||||||
|
// rewinding, silently handle
|
||||||
|
wholeSpins = spins;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (wholeSpins != spins)
|
||||||
|
{
|
||||||
|
var tick = ticks.FirstOrDefault(t => !t.IsHit);
|
||||||
|
|
||||||
|
// tick may be null if we've hit the spin limit.
|
||||||
|
if (tick != null)
|
||||||
|
{
|
||||||
|
tick.TriggerResult(true);
|
||||||
|
if (tick is DrawableSpinnerBonusTick)
|
||||||
|
bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
wholeSpins++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void UpdateInitialTransforms()
|
protected override void UpdateInitialTransforms()
|
||||||
{
|
{
|
||||||
base.UpdateInitialTransforms();
|
base.UpdateInitialTransforms();
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
|
{
|
||||||
|
public class DrawableSpinnerBonusTick : DrawableSpinnerTick
|
||||||
|
{
|
||||||
|
public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick)
|
||||||
|
: base(spinnerTick)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
|
{
|
||||||
|
public class DrawableSpinnerTick : DrawableOsuHitObject
|
||||||
|
{
|
||||||
|
public override bool DisplayResult => false;
|
||||||
|
|
||||||
|
public DrawableSpinnerTick(SpinnerTick spinnerTick)
|
||||||
|
: base(spinnerTick)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply a judgement result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hit">Whether this tick was reached.</param>
|
||||||
|
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss);
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
private readonly Slider slider;
|
private readonly 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;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shows incremental bonus score achieved for a spinner.
|
||||||
|
/// </summary>
|
||||||
|
public class SpinnerBonusDisplay : CompositeDrawable
|
||||||
|
{
|
||||||
|
private readonly OsuSpriteText bonusCounter;
|
||||||
|
|
||||||
|
public SpinnerBonusDisplay()
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChild = bonusCounter = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Font = OsuFont.Numeric.With(size: 24),
|
||||||
|
Alpha = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int displayedCount;
|
||||||
|
|
||||||
|
public void SetBonusCount(int count)
|
||||||
|
{
|
||||||
|
if (displayedCount == count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
displayedCount = count;
|
||||||
|
bonusCounter.Text = $"{1000 * count}";
|
||||||
|
bonusCounter.FadeOutFromOne(1500);
|
||||||
|
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
26
osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
Normal file
26
osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects
|
||||||
|
{
|
||||||
|
public class SpinnerBonusTick : SpinnerTick
|
||||||
|
{
|
||||||
|
public SpinnerBonusTick()
|
||||||
|
{
|
||||||
|
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
|
||||||
|
|
||||||
|
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
|
||||||
|
{
|
||||||
|
protected override int NumericResultFor(HitResult result) => 1100;
|
||||||
|
|
||||||
|
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
Normal file
25
osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Objects
|
||||||
|
{
|
||||||
|
public class SpinnerTick : OsuHitObject
|
||||||
|
{
|
||||||
|
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
|
||||||
|
|
||||||
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
|
||||||
|
public class OsuSpinnerTickJudgement : OsuJudgement
|
||||||
|
{
|
||||||
|
public override bool AffectsCombo => false;
|
||||||
|
|
||||||
|
protected override int NumericResultFor(HitResult result) => 100;
|
||||||
|
|
||||||
|
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
|||||||
/// </summary>
|
/// </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.
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 |
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
Normal file
64
osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -65,11 +64,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private void bindHandler(double delay = 0)
|
private void bindHandler(double delay = 0)
|
||||||
{
|
{
|
||||||
var roomScores = new List<RoomScore>();
|
var roomScores = new List<MultiplayerScore>();
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
roomScores.Add(new RoomScore
|
roomScores.Add(new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = i,
|
ID = i,
|
||||||
Accuracy = 0.9 - 0.01 * i,
|
Accuracy = 0.9 - 0.01 * i,
|
||||||
|
@ -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";
|
||||||
|
278
osu.Game/Beatmaps/BeatmapDifficultyManager.cs
Normal file
278
osu.Game/Beatmaps/BeatmapDifficultyManager.cs
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Lists;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps
|
||||||
|
{
|
||||||
|
public class BeatmapDifficultyManager : CompositeDrawable
|
||||||
|
{
|
||||||
|
// Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
|
||||||
|
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager));
|
||||||
|
|
||||||
|
// A permanent cache to prevent re-computations.
|
||||||
|
private readonly ConcurrentDictionary<DifficultyCacheLookup, StarDifficulty> difficultyCache = new ConcurrentDictionary<DifficultyCacheLookup, StarDifficulty>();
|
||||||
|
|
||||||
|
// All bindables that should be updated along with the current ruleset + mods.
|
||||||
|
private readonly LockedWeakList<BindableStarDifficulty> trackedBindables = new LockedWeakList<BindableStarDifficulty>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmapManager { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private Bindable<RulesetInfo> currentRuleset { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private Bindable<IReadOnlyList<Mod>> currentMods { get; set; }
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
currentRuleset.BindValueChanged(_ => updateTrackedBindables());
|
||||||
|
currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> that follows the currently-selected ruleset and mods.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||||
|
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||||
|
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
|
||||||
|
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
|
||||||
|
trackedBindables.Add(bindable);
|
||||||
|
return bindable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The bindable will not update to follow the currently-selected ruleset and mods.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||||
|
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
|
||||||
|
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
|
||||||
|
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||||
|
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
|
||||||
|
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
=> createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||||
|
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
|
||||||
|
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
|
||||||
|
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops computing the star difficulty.</param>
|
||||||
|
/// <returns>The <see cref="StarDifficulty"/>.</returns>
|
||||||
|
public async Task<StarDifficulty> GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable<Mod> mods = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken,
|
||||||
|
TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||||
|
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
|
||||||
|
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
|
||||||
|
/// <returns>The <see cref="StarDifficulty"/>.</returns>
|
||||||
|
public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable<Mod> mods = null)
|
||||||
|
{
|
||||||
|
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
return computeDifficulty(key, beatmapInfo, rulesetInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CancellationTokenSource trackedUpdateCancellationSource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates all tracked <see cref="BindableStarDifficulty"/> using the current ruleset and mods.
|
||||||
|
/// </summary>
|
||||||
|
private void updateTrackedBindables()
|
||||||
|
{
|
||||||
|
trackedUpdateCancellationSource?.Cancel();
|
||||||
|
trackedUpdateCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
foreach (var b in trackedBindables)
|
||||||
|
{
|
||||||
|
if (trackedUpdateCancellationSource.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken))
|
||||||
|
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the value of a <see cref="BindableStarDifficulty"/> with a given ruleset + mods.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
|
||||||
|
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to update with.</param>
|
||||||
|
/// <param name="mods">The <see cref="Mod"/>s to update with.</param>
|
||||||
|
/// <param name="cancellationToken">A token that may be used to cancel this update.</param>
|
||||||
|
private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t =>
|
||||||
|
{
|
||||||
|
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (!cancellationToken.IsCancellationRequested)
|
||||||
|
bindable.Value = t.Result;
|
||||||
|
});
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="BindableStarDifficulty"/> and triggers an initial value update.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> that star difficulty should correspond to.</param>
|
||||||
|
/// <param name="initialRulesetInfo">The initial <see cref="RulesetInfo"/> to get the difficulty with.</param>
|
||||||
|
/// <param name="initialMods">The initial <see cref="Mod"/>s to get the difficulty with.</param>
|
||||||
|
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||||
|
/// <returns>The <see cref="BindableStarDifficulty"/>.</returns>
|
||||||
|
private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable<Mod> initialMods,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
|
||||||
|
updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken);
|
||||||
|
return bindable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the difficulty defined by a <see cref="DifficultyCacheLookup"/> key, and stores it to the timed cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The <see cref="DifficultyCacheLookup"/> that defines the computation parameters.</param>
|
||||||
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to compute the difficulty of.</param>
|
||||||
|
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to compute the difficulty with.</param>
|
||||||
|
/// <returns>The <see cref="StarDifficulty"/>.</returns>
|
||||||
|
private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo)
|
||||||
|
{
|
||||||
|
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
|
||||||
|
rulesetInfo ??= beatmapInfo.Ruleset;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ruleset = rulesetInfo.CreateInstance();
|
||||||
|
Debug.Assert(ruleset != null);
|
||||||
|
|
||||||
|
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo));
|
||||||
|
var attributes = calculator.Calculate(key.Mods);
|
||||||
|
|
||||||
|
return difficultyCache[key] = new StarDifficulty(attributes.StarRating);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return difficultyCache[key] = new StarDifficulty(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve an existing difficulty for the combination.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/>.</param>
|
||||||
|
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/>.</param>
|
||||||
|
/// <param name="mods">The <see cref="Mod"/>s.</param>
|
||||||
|
/// <param name="existingDifficulty">The existing difficulty value, if present.</param>
|
||||||
|
/// <param name="key">The <see cref="DifficultyCacheLookup"/> key that was used to perform this lookup. This can be further used to query <see cref="computeDifficulty"/>.</param>
|
||||||
|
/// <returns>Whether an existing difficulty was found.</returns>
|
||||||
|
private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable<Mod> mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key)
|
||||||
|
{
|
||||||
|
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
|
||||||
|
rulesetInfo ??= beatmapInfo.Ruleset;
|
||||||
|
|
||||||
|
// Difficulty can only be computed if the beatmap and ruleset are locally available.
|
||||||
|
if (beatmapInfo.ID == 0 || rulesetInfo.ID == null)
|
||||||
|
{
|
||||||
|
// If not, fall back to the existing star difficulty (e.g. from an online source).
|
||||||
|
existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty);
|
||||||
|
key = default;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods);
|
||||||
|
return difficultyCache.TryGetValue(key, out existingDifficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct DifficultyCacheLookup : IEquatable<DifficultyCacheLookup>
|
||||||
|
{
|
||||||
|
public readonly int BeatmapId;
|
||||||
|
public readonly int RulesetId;
|
||||||
|
public readonly Mod[] Mods;
|
||||||
|
|
||||||
|
public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable<Mod> mods)
|
||||||
|
{
|
||||||
|
BeatmapId = beatmapId;
|
||||||
|
RulesetId = rulesetId;
|
||||||
|
Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty<Mod>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(DifficultyCacheLookup other)
|
||||||
|
=> BeatmapId == other.BeatmapId
|
||||||
|
&& RulesetId == other.RulesetId
|
||||||
|
&& Mods.SequenceEqual(other.Mods);
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
var hashCode = new HashCode();
|
||||||
|
|
||||||
|
hashCode.Add(BeatmapId);
|
||||||
|
hashCode.Add(RulesetId);
|
||||||
|
foreach (var mod in Mods)
|
||||||
|
hashCode.Add(mod.Acronym);
|
||||||
|
|
||||||
|
return hashCode.ToHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BindableStarDifficulty : Bindable<StarDifficulty>
|
||||||
|
{
|
||||||
|
public readonly BeatmapInfo Beatmap;
|
||||||
|
public readonly CancellationToken CancellationToken;
|
||||||
|
|
||||||
|
public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Beatmap = beatmap;
|
||||||
|
CancellationToken = cancellationToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct StarDifficulty
|
||||||
|
{
|
||||||
|
public readonly double Stars;
|
||||||
|
|
||||||
|
public StarDifficulty(double stars)
|
||||||
|
{
|
||||||
|
Stars = stars;
|
||||||
|
|
||||||
|
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -99,6 +99,7 @@ namespace osu.Game.Configuration
|
|||||||
Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
|
Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
|
||||||
|
|
||||||
Set(OsuSetting.IncreaseFirstObjectVisibility, true);
|
Set(OsuSetting.IncreaseFirstObjectVisibility, true);
|
||||||
|
Set(OsuSetting.GameplayDisableWinKey, true);
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
||||||
@ -229,6 +230,7 @@ namespace osu.Game.Configuration
|
|||||||
IntroSequence,
|
IntroSequence,
|
||||||
UIHoldActivationDelay,
|
UIHoldActivationDelay,
|
||||||
HitLighting,
|
HitLighting,
|
||||||
MenuBackgroundSource
|
MenuBackgroundSource,
|
||||||
|
GameplayDisableWinKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// 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.Linq;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
@ -55,7 +54,16 @@ namespace osu.Game.Graphics.Cursor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newTarget = inputManager.HoveredDrawables.OfType<IProvideCursor>().FirstOrDefault(t => t.ProvidingUserCursor) ?? this;
|
IProvideCursor newTarget = this;
|
||||||
|
|
||||||
|
foreach (var d in inputManager.HoveredDrawables)
|
||||||
|
{
|
||||||
|
if (d is IProvideCursor p && p.ProvidingUserCursor)
|
||||||
|
{
|
||||||
|
newTarget = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentTarget == newTarget)
|
if (currentTarget == newTarget)
|
||||||
return;
|
return;
|
||||||
|
@ -19,6 +19,7 @@ using osu.Game.Input.Bindings;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
|
|
||||||
namespace osu.Game.Graphics
|
namespace osu.Game.Graphics
|
||||||
{
|
{
|
||||||
@ -119,7 +120,9 @@ namespace osu.Game.Graphics
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ScreenshotFormat.Jpg:
|
case ScreenshotFormat.Jpg:
|
||||||
image.SaveAsJpeg(stream);
|
const int jpeg_quality = 92;
|
||||||
|
|
||||||
|
image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
// 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 Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests.Responses
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class APICreatedRoom : Room
|
public class APICreatedRoom : Room
|
||||||
{
|
{
|
@ -6,7 +6,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class APIPlaylistBeatmap : APIBeatmap
|
public class APIPlaylistBeatmap : APIBeatmap
|
||||||
{
|
{
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests.Responses
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class APIScoreToken
|
public class APIScoreToken
|
||||||
{
|
{
|
@ -4,10 +4,9 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class CreateRoomRequest : APIRequest<APICreatedRoom>
|
public class CreateRoomRequest : APIRequest<APICreatedRoom>
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class CreateRoomScoreRequest : APIRequest<APIScoreToken>
|
public class CreateRoomScoreRequest : APIRequest<APIScoreToken>
|
||||||
{
|
{
|
@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class GetRoomPlaylistScoresRequest : APIRequest<RoomPlaylistScores>
|
public class GetRoomPlaylistScoresRequest : APIRequest<RoomPlaylistScores>
|
||||||
{
|
{
|
||||||
@ -23,6 +24,6 @@ namespace osu.Game.Online.API.Requests
|
|||||||
public class RoomPlaylistScores
|
public class RoomPlaylistScores
|
||||||
{
|
{
|
||||||
[JsonProperty("scores")]
|
[JsonProperty("scores")]
|
||||||
public List<RoomScore> Scores { get; set; }
|
public List<MultiplayerScore> Scores { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +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.Game.Online.Multiplayer;
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class GetRoomRequest : APIRequest<Room>
|
public class GetRoomRequest : APIRequest<Room>
|
||||||
{
|
{
|
@ -2,9 +2,10 @@
|
|||||||
// 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 osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class GetRoomScoresRequest : APIRequest<List<APIUserScoreAggregate>>
|
public class GetRoomScoresRequest : APIRequest<List<APIUserScoreAggregate>>
|
||||||
{
|
{
|
@ -4,10 +4,10 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Screens.Multi.Lounge.Components;
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class GetRoomsRequest : APIRequest<List<Room>>
|
public class GetRoomsRequest : APIRequest<List<Room>>
|
||||||
{
|
{
|
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class JoinRoomRequest : APIRequest
|
public class JoinRoomRequest : APIRequest
|
||||||
{
|
{
|
@ -6,15 +6,15 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class RoomScore
|
public class MultiplayerScore
|
||||||
{
|
{
|
||||||
[JsonProperty("id")]
|
[JsonProperty("id")]
|
||||||
public int ID { get; set; }
|
public int ID { get; set; }
|
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.API;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class PartRoomRequest : APIRequest
|
public class PartRoomRequest : APIRequest
|
||||||
{
|
{
|
@ -4,11 +4,12 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
public class SubmitRoomScoreRequest : APIRequest<RoomScore>
|
public class SubmitRoomScoreRequest : APIRequest<MultiplayerScore>
|
||||||
{
|
{
|
||||||
private readonly int scoreId;
|
private readonly int scoreId;
|
||||||
private readonly int roomId;
|
private readonly int roomId;
|
@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Humanizer;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -759,7 +760,7 @@ namespace osu.Game
|
|||||||
Schedule(() => notifications.Post(new SimpleNotification
|
Schedule(() => notifications.Post(new SimpleNotification
|
||||||
{
|
{
|
||||||
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
|
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
|
||||||
Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
|
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
else if (recentLogCount == short_term_display_limit)
|
else if (recentLogCount == short_term_display_limit)
|
||||||
|
@ -199,6 +199,10 @@ namespace osu.Game
|
|||||||
ScoreManager.Undelete(getBeatmapScores(item), true);
|
ScoreManager.Undelete(getBeatmapScores(item), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var difficultyManager = new BeatmapDifficultyManager();
|
||||||
|
dependencies.Cache(difficultyManager);
|
||||||
|
AddInternal(difficultyManager);
|
||||||
|
|
||||||
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
|
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
|
||||||
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
|
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
|
||||||
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
|
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
|
||||||
|
@ -12,9 +12,26 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
public abstract class OverlayHeader : Container
|
public abstract class OverlayHeader : Container
|
||||||
{
|
{
|
||||||
public const int CONTENT_X_MARGIN = 50;
|
private float contentSidePadding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Horizontal padding of the header content.
|
||||||
|
/// </summary>
|
||||||
|
protected float ContentSidePadding
|
||||||
|
{
|
||||||
|
get => contentSidePadding;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
contentSidePadding = value;
|
||||||
|
content.Padding = new MarginPadding
|
||||||
|
{
|
||||||
|
Horizontal = value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly Box titleBackground;
|
private readonly Box titleBackground;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
protected readonly FillFlowContainer HeaderInfo;
|
protected readonly FillFlowContainer HeaderInfo;
|
||||||
|
|
||||||
@ -50,14 +67,10 @@ namespace osu.Game.Overlays
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.Gray,
|
Colour = Color4.Gray,
|
||||||
},
|
},
|
||||||
new Container
|
content = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Padding = new MarginPadding
|
|
||||||
{
|
|
||||||
Horizontal = CONTENT_X_MARGIN,
|
|
||||||
},
|
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
CreateTitle().With(title =>
|
CreateTitle().With(title =>
|
||||||
@ -79,6 +92,8 @@ namespace osu.Game.Overlays
|
|||||||
CreateContent()
|
CreateContent()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ContentSidePadding = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Profile
|
|||||||
|
|
||||||
public ProfileHeader()
|
public ProfileHeader()
|
||||||
{
|
{
|
||||||
|
ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
|
||||||
|
|
||||||
User.ValueChanged += e => updateDisplay(e.NewValue);
|
User.ValueChanged += e => updateDisplay(e.NewValue);
|
||||||
|
|
||||||
TabControl.AddItem("info");
|
TabControl.AddItem("info");
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -78,6 +79,15 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
Bindable = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode)
|
Bindable = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
|
||||||
|
{
|
||||||
|
Add(new SettingsCheckbox
|
||||||
|
{
|
||||||
|
LabelText = "Disable Windows key during gameplay",
|
||||||
|
Bindable = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ namespace osu.Game.Overlays
|
|||||||
protected OsuTabControl<T> TabControl;
|
protected OsuTabControl<T> TabControl;
|
||||||
|
|
||||||
private readonly Box controlBackground;
|
private readonly Box controlBackground;
|
||||||
|
private readonly Container tabControlContainer;
|
||||||
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
|
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
|
||||||
|
|
||||||
public Bindable<T> Current
|
public Bindable<T> Current
|
||||||
@ -30,6 +31,16 @@ namespace osu.Game.Overlays
|
|||||||
set => current.Current = value;
|
set => current.Current = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected new float ContentSidePadding
|
||||||
|
{
|
||||||
|
get => base.ContentSidePadding;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.ContentSidePadding = value;
|
||||||
|
tabControlContainer.Padding = new MarginPadding { Horizontal = value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected TabControlOverlayHeader()
|
protected TabControlOverlayHeader()
|
||||||
{
|
{
|
||||||
HeaderInfo.Add(new Container
|
HeaderInfo.Add(new Container
|
||||||
@ -42,11 +53,16 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
TabControl = CreateTabControl().With(control =>
|
tabControlContainer = new Container
|
||||||
{
|
{
|
||||||
control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN };
|
RelativeSizeAxes = Axes.X,
|
||||||
control.Current = Current;
|
AutoSizeAxes = Axes.Y,
|
||||||
})
|
Padding = new MarginPadding { Horizontal = ContentSidePadding },
|
||||||
|
Child = TabControl = CreateTabControl().With(control =>
|
||||||
|
{
|
||||||
|
control.Current = Current;
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
|
|
||||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||||
=> !(result.Judgement is IgnoreJudgement)
|
=> !(result.Judgement is IgnoreJudgement)
|
||||||
|
&& result.Judgement.AffectsCombo
|
||||||
&& result.Type != result.Judgement.MaxResult;
|
&& result.Type != result.Judgement.MaxResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,12 +126,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
if (Result == null)
|
if (Result == null)
|
||||||
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
||||||
|
|
||||||
loadSamples();
|
LoadSamples();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadAsyncComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadAsyncComplete();
|
||||||
|
|
||||||
HitObject.DefaultsApplied += onDefaultsApplied;
|
HitObject.DefaultsApplied += onDefaultsApplied;
|
||||||
|
|
||||||
@ -145,14 +145,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
|
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
|
||||||
samplesBindable.CollectionChanged += (_, __) => loadSamples();
|
samplesBindable.CollectionChanged += (_, __) => LoadSamples();
|
||||||
|
|
||||||
apply(HitObject);
|
apply(HitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
updateState(ArmedState.Idle, true);
|
updateState(ArmedState.Idle, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSamples()
|
protected virtual void LoadSamples()
|
||||||
{
|
{
|
||||||
if (Samples != null)
|
if (Samples != null)
|
||||||
{
|
{
|
||||||
@ -353,17 +358,32 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private GameplayClock gameplayClock { get; set; }
|
private GameplayClock gameplayClock { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculate the position to be used for sample playback at a specified X position (0..1).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">The lookup X position. Generally should be <see cref="SamplePlaybackPosition"/>.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected double CalculateSamplePlaybackBalance(double position)
|
||||||
|
{
|
||||||
|
const float balance_adjust_amount = 0.4f;
|
||||||
|
|
||||||
|
return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether samples should currently be playing. Will be false during seek operations.
|
||||||
|
/// </summary>
|
||||||
|
protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
|
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
|
||||||
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
|
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void PlaySamples()
|
public virtual void PlaySamples()
|
||||||
{
|
{
|
||||||
const float balance_adjust_amount = 0.4f;
|
if (Samples != null && ShouldPlaySamples)
|
||||||
|
|
||||||
if (Samples != null && gameplayClock?.IsSeeking != true)
|
|
||||||
{
|
{
|
||||||
Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0);
|
Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
|
||||||
Samples.Play();
|
Samples.Play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
// 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 JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Layout;
|
using osu.Framework.Layout;
|
||||||
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
{
|
{
|
||||||
private readonly IBindable<double> timeRange = new BindableDouble();
|
private readonly IBindable<double> timeRange = new BindableDouble();
|
||||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||||
private readonly Dictionary<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>();
|
private readonly Dictionary<DrawableHitObject, InitialState> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, InitialState>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IScrollingInfo scrollingInfo { get; set; }
|
private IScrollingInfo scrollingInfo { get; set; }
|
||||||
@ -175,10 +177,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
{
|
{
|
||||||
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
|
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
|
||||||
// In such a case, combinedObjCache will take care of updating the hitobject.
|
// In such a case, combinedObjCache will take care of updating the hitobject.
|
||||||
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache))
|
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state))
|
||||||
{
|
{
|
||||||
combinedObjCache.Invalidate();
|
combinedObjCache.Invalidate();
|
||||||
objCache.Invalidate();
|
state.Cache.Invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,8 +192,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
|
|
||||||
if (!layoutCache.IsValid)
|
if (!layoutCache.IsValid)
|
||||||
{
|
{
|
||||||
foreach (var cached in hitObjectInitialStateCache.Values)
|
foreach (var state in hitObjectInitialStateCache.Values)
|
||||||
cached.Invalidate();
|
state.Cache.Invalidate();
|
||||||
combinedObjCache.Invalidate();
|
combinedObjCache.Invalidate();
|
||||||
|
|
||||||
scrollingInfo.Algorithm.Reset();
|
scrollingInfo.Algorithm.Reset();
|
||||||
@ -215,16 +217,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
|
|
||||||
foreach (var obj in Objects)
|
foreach (var obj in Objects)
|
||||||
{
|
{
|
||||||
if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache))
|
if (!hitObjectInitialStateCache.TryGetValue(obj, out var state))
|
||||||
objCache = hitObjectInitialStateCache[obj] = new Cached();
|
state = hitObjectInitialStateCache[obj] = new InitialState(new Cached());
|
||||||
|
|
||||||
if (objCache.IsValid)
|
if (state.Cache.IsValid)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
computeLifetimeStartRecursive(obj);
|
state.ScheduledComputation?.Cancel();
|
||||||
computeInitialStateRecursive(obj);
|
state.ScheduledComputation = computeInitialStateRecursive(obj);
|
||||||
|
|
||||||
objCache.Validate();
|
computeLifetimeStartRecursive(obj);
|
||||||
|
|
||||||
|
state.Cache.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
combinedObjCache.Validate();
|
combinedObjCache.Validate();
|
||||||
@ -267,8 +271,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
|
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cant use AddOnce() since the delegate is re-constructed every invocation
|
private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
|
||||||
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
|
|
||||||
{
|
{
|
||||||
if (hitObject.HitObject is IHasDuration e)
|
if (hitObject.HitObject is IHasDuration e)
|
||||||
{
|
{
|
||||||
@ -325,5 +328,19 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class InitialState
|
||||||
|
{
|
||||||
|
[NotNull]
|
||||||
|
public readonly Cached Cache;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
public ScheduledDelegate ScheduledComputation;
|
||||||
|
|
||||||
|
public InitialState(Cached cache)
|
||||||
|
{
|
||||||
|
Cache = cache;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Leaderboards;
|
using osu.Game.Online.Leaderboards;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
@ -10,7 +10,6 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
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.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
|
@ -14,7 +14,6 @@ using osu.Framework.Logging;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Screens.Multi.Lounge.Components;
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Ranking.Contracted;
|
using osu.Game.Screens.Ranking.Contracted;
|
||||||
using osu.Game.Screens.Ranking.Expanded;
|
using osu.Game.Screens.Ranking.Expanded;
|
||||||
|
using osu.Game.Users;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -142,7 +143,16 @@ namespace osu.Game.Screens.Ranking
|
|||||||
CornerRadius = 20,
|
CornerRadius = 20,
|
||||||
CornerExponent = 2.5f,
|
CornerExponent = 2.5f,
|
||||||
Masking = true,
|
Masking = true,
|
||||||
Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }
|
Children = new[]
|
||||||
|
{
|
||||||
|
middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both },
|
||||||
|
new UserCoverBackground
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
User = Score.User,
|
||||||
|
Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
|
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
}
|
}
|
||||||
@ -155,18 +165,10 @@ namespace osu.Game.Screens.Ranking
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
if (state == PanelState.Expanded)
|
|
||||||
{
|
|
||||||
topLayerBackground.FadeColour(expanded_top_layer_colour);
|
|
||||||
middleLayerBackground.FadeColour(expanded_middle_layer_colour);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
topLayerBackground.FadeColour(contracted_top_layer_colour);
|
|
||||||
middleLayerBackground.FadeColour(contracted_middle_layer_colour);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState();
|
updateState();
|
||||||
|
|
||||||
|
topLayerBackground.FinishTransforms(false, nameof(Colour));
|
||||||
|
middleLayerBackground.FinishTransforms(false, nameof(Colour));
|
||||||
}
|
}
|
||||||
|
|
||||||
private PanelState state = PanelState.Contracted;
|
private PanelState state = PanelState.Contracted;
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
@ -41,6 +43,12 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private BeatmapSetOverlay beatmapOverlay { get; set; }
|
private BeatmapSetOverlay beatmapOverlay { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapDifficultyManager difficultyManager { get; set; }
|
||||||
|
|
||||||
|
private IBindable<StarDifficulty> starDifficultyBindable;
|
||||||
|
private CancellationTokenSource starDifficultyCancellationSource;
|
||||||
|
|
||||||
public DrawableCarouselBeatmap(CarouselBeatmap panel)
|
public DrawableCarouselBeatmap(CarouselBeatmap panel)
|
||||||
: base(panel)
|
: base(panel)
|
||||||
{
|
{
|
||||||
@ -137,7 +145,6 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
},
|
},
|
||||||
starCounter = new StarCounter
|
starCounter = new StarCounter
|
||||||
{
|
{
|
||||||
Current = (float)beatmap.StarDifficulty,
|
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(0.8f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,6 +188,16 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0)
|
if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0)
|
||||||
starCounter.ReplayAnimation();
|
starCounter.ReplayAnimation();
|
||||||
|
|
||||||
|
starDifficultyCancellationSource?.Cancel();
|
||||||
|
|
||||||
|
// Only compute difficulty when the item is visible.
|
||||||
|
if (Item.State.Value != CarouselItemState.Collapsed)
|
||||||
|
{
|
||||||
|
// We've potentially cancelled the computation above so a new bindable is required.
|
||||||
|
starDifficultyBindable = difficultyManager.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token);
|
||||||
|
starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true);
|
||||||
|
}
|
||||||
|
|
||||||
base.ApplyState();
|
base.ApplyState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,5 +222,11 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
return items.ToArray();
|
return items.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
starDifficultyCancellationSource?.Cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,12 @@ using osu.Framework.Bindables;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Details
|
namespace osu.Game.Screens.Select.Details
|
||||||
{
|
{
|
||||||
@ -26,6 +28,12 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapDifficultyManager difficultyManager { get; set; }
|
||||||
|
|
||||||
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
|
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
|
||||||
private readonly StatisticRow starDifficulty;
|
private readonly StatisticRow starDifficulty;
|
||||||
|
|
||||||
@ -71,6 +79,7 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ruleset.BindValueChanged(_ => updateStatistics());
|
||||||
mods.BindValueChanged(modsChanged, true);
|
mods.BindValueChanged(modsChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,11 +141,39 @@ namespace osu.Game.Screens.Select.Details
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
starDifficulty.Value = ((float)(Beatmap?.StarDifficulty ?? 0), null);
|
|
||||||
|
|
||||||
HpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate);
|
HpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate);
|
||||||
Accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty);
|
Accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty);
|
||||||
ApproachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate);
|
ApproachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate);
|
||||||
|
|
||||||
|
updateStarDifficulty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IBindable<StarDifficulty> normalStarDifficulty;
|
||||||
|
private IBindable<StarDifficulty> moddedStarDifficulty;
|
||||||
|
private CancellationTokenSource starDifficultyCancellationSource;
|
||||||
|
|
||||||
|
private void updateStarDifficulty()
|
||||||
|
{
|
||||||
|
starDifficultyCancellationSource?.Cancel();
|
||||||
|
|
||||||
|
if (Beatmap == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token);
|
||||||
|
moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
||||||
|
|
||||||
|
normalStarDifficulty.BindValueChanged(_ => updateDisplay());
|
||||||
|
moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true);
|
||||||
|
|
||||||
|
void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
starDifficultyCancellationSource?.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StatisticRow : Container, IHasAccentColour
|
public class StatisticRow : Container, IHasAccentColour
|
||||||
|
@ -22,6 +22,9 @@ namespace osu.Game.Skinning
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private ISampleStore samples { get; set; }
|
private ISampleStore samples { get; set; }
|
||||||
|
|
||||||
|
public override bool RemoveWhenNotAlive => false;
|
||||||
|
public override bool RemoveCompletedTransforms => false;
|
||||||
|
|
||||||
public SkinnableSound(ISampleInfo hitSamples)
|
public SkinnableSound(ISampleInfo hitSamples)
|
||||||
: this(new[] { hitSamples })
|
: this(new[] { hitSamples })
|
||||||
{
|
{
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2020.722.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2020.723.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" />
|
||||||
<PackageReference Include="Sentry" Version="2.1.4" />
|
<PackageReference Include="Sentry" Version="2.1.4" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.25.1" />
|
<PackageReference Include="SharpCompress" Version="0.25.1" />
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.722.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.723.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
||||||
@ -80,7 +80,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2020.722.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2020.723.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.25.1" />
|
<PackageReference Include="SharpCompress" Version="0.25.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user