mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 10:23:20 +08:00
Merge branch 'master' into replay-analysis-spectating
This commit is contained in:
commit
9e262483e5
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.904.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.916.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -95,11 +95,11 @@ namespace osu.Desktop
|
||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
}
|
||||
|
||||
public static bool IsPackageManaged => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"));
|
||||
|
||||
protected override UpdateManager CreateUpdateManager()
|
||||
{
|
||||
string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER");
|
||||
|
||||
if (!string.IsNullOrEmpty(packageManaged))
|
||||
if (IsPackageManaged)
|
||||
return new NoActionUpdateManager();
|
||||
|
||||
return new VelopackUpdateManager();
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework;
|
||||
@ -168,12 +169,26 @@ namespace osu.Desktop
|
||||
|
||||
private static void setupVelopack()
|
||||
{
|
||||
VelopackApp
|
||||
.Build()
|
||||
.WithFirstRun(v =>
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations();
|
||||
}).Run();
|
||||
if (OsuGameDesktop.IsPackageManaged)
|
||||
{
|
||||
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
|
||||
return;
|
||||
}
|
||||
|
||||
var app = VelopackApp.Build();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
configureWindows(app);
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void configureWindows(VelopackApp app)
|
||||
{
|
||||
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
||||
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
||||
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,14 +45,17 @@ namespace osu.Desktop.Updater
|
||||
|
||||
private async Task<bool> checkForUpdateAsync(UpdateProgressNotification? notification = null)
|
||||
{
|
||||
// should we schedule a retry on completion of this check?
|
||||
bool scheduleRecheck = true;
|
||||
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
|
||||
bool scheduleRecheck = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Avoid any kind of update checking while gameplay is running.
|
||||
if (localUserInfo?.IsPlaying.Value == true)
|
||||
{
|
||||
scheduleRecheck = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
||||
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
|
||||
@ -67,17 +70,20 @@ namespace osu.Desktop.Updater
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||
|
||||
// Handle no updates available.
|
||||
// No update is available. We'll check again later.
|
||||
if (pendingUpdate == null)
|
||||
{
|
||||
scheduleRecheck = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduleRecheck = false;
|
||||
|
||||
// An update is found, let's notify the user and start downloading it.
|
||||
if (notification == null)
|
||||
{
|
||||
notification = new UpdateProgressNotification
|
||||
@ -99,6 +105,7 @@ namespace osu.Desktop.Updater
|
||||
catch (Exception e)
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
scheduleRecheck = true;
|
||||
notification.FailDownload();
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
@ -113,7 +120,6 @@ namespace osu.Desktop.Updater
|
||||
{
|
||||
if (scheduleRecheck)
|
||||
{
|
||||
// check again in 30 minutes.
|
||||
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
||||
}
|
||||
}
|
||||
|
@ -13,5 +13,7 @@ namespace osu.Desktop.Windows
|
||||
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||
|
||||
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||
|
||||
public static string Beatmap => Path.Join(icon_directory, "beatmap.ico");
|
||||
}
|
||||
}
|
||||
|
@ -40,10 +40,10 @@ namespace osu.Desktop.Windows
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Beatmap),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Beatmap),
|
||||
};
|
||||
|
||||
private static readonly UriAssociation[] uri_associations =
|
||||
|
BIN
osu.Desktop/beatmap.ico
Normal file
BIN
osu.Desktop/beatmap.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 349 KiB |
@ -5,6 +5,7 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
|
||||
<AssemblyName>osu!</AssemblyName>
|
||||
<AssemblyTitle>osu!(lazer)</AssemblyTitle>
|
||||
<Title>osu!</Title>
|
||||
<Product>osu!(lazer)</Product>
|
||||
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double star_scaling_factor = 0.153;
|
||||
private const double difficulty_multiplier = 4.59;
|
||||
|
||||
private float halfCatcherWidth;
|
||||
|
||||
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
protected override double SkillMultiplier => 900;
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.2;
|
||||
|
||||
protected override double DecayWeight => 0.94;
|
||||
|
@ -1,10 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
{
|
||||
@ -36,23 +38,43 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
StartTimeBindable.BindValueChanged(_ => UpdateComboColour());
|
||||
}
|
||||
|
||||
private float startScale;
|
||||
private float endScale;
|
||||
|
||||
private float startAngle;
|
||||
private float endAngle;
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||
const float end_scale = 0.6f;
|
||||
const float random_scale_range = 1.6f;
|
||||
|
||||
ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
|
||||
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
|
||||
startScale = end_scale + random_scale_range * RandomSingle(3);
|
||||
endScale = end_scale;
|
||||
|
||||
ScalingContainer.RotateTo(getRandomAngle(1))
|
||||
.Then()
|
||||
.RotateTo(getRandomAngle(2), HitObject.TimePreempt);
|
||||
startAngle = getRandomAngle(1);
|
||||
endAngle = getRandomAngle(2);
|
||||
|
||||
float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt;
|
||||
|
||||
// Clamp scale and rotation at the point of bananas being caught, else let them freely extrapolate.
|
||||
if (Result.IsHit)
|
||||
preemptProgress = Math.Min(1, preemptProgress);
|
||||
|
||||
ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress));
|
||||
ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress);
|
||||
}
|
||||
|
||||
public override void PlaySamples()
|
||||
{
|
||||
base.PlaySamples();
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -28,15 +28,24 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
_ => new DropletPiece());
|
||||
}
|
||||
|
||||
private float startRotation;
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// roughly matches osu-stable
|
||||
float startRotation = RandomSingle(1) * 20;
|
||||
double duration = HitObject.TimePreempt + 2000;
|
||||
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||
startRotation = RandomSingle(1) * 20;
|
||||
}
|
||||
|
||||
ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// No clamping for droplets. They should be considered indefinitely spinning regardless of time.
|
||||
// They also never end up on the plate, so they shouldn't stop spinning when caught.
|
||||
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000);
|
||||
ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -32,7 +31,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
|
||||
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||
ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public class ManiaDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double star_scaling_factor = 0.018;
|
||||
private const double difficulty_multiplier = 0.018;
|
||||
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||
StarRating = skills[0].DifficultyValue() * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
|
@ -38,9 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
scoreAccuracy = calculateCustomAccuracy();
|
||||
|
||||
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
|
||||
// The specific number has no intrinsic meaning and can be adjusted as needed.
|
||||
double multiplier = 8.0;
|
||||
double multiplier = 1.0;
|
||||
|
||||
if (score.Mods.Any(m => m is ModNoFail))
|
||||
multiplier *= 0.75;
|
||||
@ -59,9 +57,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
|
||||
* Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
|
||||
* (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
|
||||
double difficultyValue = 8.0 * Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
|
||||
* Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
|
||||
* (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
|
||||
|
||||
return difficultyValue;
|
||||
}
|
||||
|
@ -2,9 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -12,17 +15,76 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyManiaComboCounter : LegacyComboCounter
|
||||
public partial class LegacyManiaComboCounter : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
DisplayedCountText.Anchor = Anchor.Centre;
|
||||
DisplayedCountText.Origin = Anchor.Centre;
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
PopOutCountText.Anchor = Anchor.Centre;
|
||||
PopOutCountText.Origin = Anchor.Centre;
|
||||
PopOutCountText.Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red;
|
||||
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0 };
|
||||
|
||||
/// <summary>
|
||||
/// Value shown at the current moment.
|
||||
/// </summary>
|
||||
public virtual int DisplayedCount
|
||||
{
|
||||
get => displayedCount;
|
||||
private set
|
||||
{
|
||||
if (displayedCount.Equals(value))
|
||||
return;
|
||||
|
||||
displayedCountText.FadeTo(value == 0 ? 0 : 1);
|
||||
displayedCountText.Text = value.ToString(CultureInfo.InvariantCulture);
|
||||
counterContainer.Size = displayedCountText.Size;
|
||||
|
||||
displayedCount = value;
|
||||
}
|
||||
}
|
||||
|
||||
private int displayedCount;
|
||||
|
||||
private int previousValue;
|
||||
|
||||
private const double fade_out_duration = 100;
|
||||
private const double rolling_duration = 20;
|
||||
|
||||
private Container counterContainer = null!;
|
||||
private LegacySpriteText popOutCountText = null!;
|
||||
private LegacySpriteText displayedCountText = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, ScoreProcessor scoreProcessor)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
counterContainer = new Container
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Children = new[]
|
||||
{
|
||||
popOutCountText = new LegacySpriteText(LegacyFont.Combo)
|
||||
{
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red,
|
||||
},
|
||||
displayedCountText = new LegacySpriteText(LegacyFont.Combo)
|
||||
{
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Current.BindTo(scoreProcessor.Combo);
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
@ -34,6 +96,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
displayedCountText.Text = popOutCountText.Text = Current.Value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
|
||||
|
||||
counterContainer.Size = displayedCountText.Size;
|
||||
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => updateAnchor());
|
||||
|
||||
@ -56,36 +124,71 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
|
||||
}
|
||||
|
||||
protected override void OnCountIncrement()
|
||||
private void updateCount(bool rolling)
|
||||
{
|
||||
base.OnCountIncrement();
|
||||
int prev = previousValue;
|
||||
previousValue = Current.Value;
|
||||
|
||||
PopOutCountText.Hide();
|
||||
DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f))
|
||||
if (!IsLoaded)
|
||||
return;
|
||||
|
||||
if (!rolling)
|
||||
{
|
||||
FinishTransforms(false, nameof(DisplayedCount));
|
||||
|
||||
if (prev + 1 == Current.Value)
|
||||
onCountIncrement();
|
||||
else
|
||||
onCountChange();
|
||||
}
|
||||
else
|
||||
onCountRolling();
|
||||
}
|
||||
|
||||
private void onCountIncrement()
|
||||
{
|
||||
popOutCountText.Hide();
|
||||
|
||||
DisplayedCount = Current.Value;
|
||||
displayedCountText.ScaleTo(new Vector2(1f, 1.4f))
|
||||
.ScaleTo(new Vector2(1f), 300, Easing.Out)
|
||||
.FadeIn(120);
|
||||
}
|
||||
|
||||
protected override void OnCountChange()
|
||||
private void onCountChange()
|
||||
{
|
||||
base.OnCountChange();
|
||||
popOutCountText.Hide();
|
||||
|
||||
PopOutCountText.Hide();
|
||||
DisplayedCountText.ScaleTo(1f);
|
||||
if (Current.Value == 0)
|
||||
displayedCountText.FadeOut();
|
||||
|
||||
DisplayedCount = Current.Value;
|
||||
|
||||
displayedCountText.ScaleTo(1f);
|
||||
}
|
||||
|
||||
protected override void OnCountRolling()
|
||||
private void onCountRolling()
|
||||
{
|
||||
if (DisplayedCount > 0)
|
||||
{
|
||||
PopOutCountText.Text = FormatCount(DisplayedCount);
|
||||
PopOutCountText.FadeTo(0.8f).FadeOut(200)
|
||||
popOutCountText.Text = DisplayedCount.ToString(CultureInfo.InvariantCulture);
|
||||
popOutCountText.FadeTo(0.8f).FadeOut(200)
|
||||
.ScaleTo(1f).ScaleTo(4f, 200);
|
||||
|
||||
DisplayedCountText.FadeTo(0.5f, 300);
|
||||
displayedCountText.FadeTo(0.5f, 300);
|
||||
}
|
||||
|
||||
base.OnCountRolling();
|
||||
// Hides displayed count if was increasing from 0 to 1 but didn't finish
|
||||
if (DisplayedCount == 0 && Current.Value == 0)
|
||||
displayedCountText.FadeOut(fade_out_duration);
|
||||
|
||||
this.TransformTo(nameof(DisplayedCount), Current.Value, getProportionalDuration(DisplayedCount, Current.Value));
|
||||
}
|
||||
|
||||
private double getProportionalDuration(int currentValue, int newValue)
|
||||
{
|
||||
double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
|
||||
return difference * rolling_duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
|
||||
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
|
||||
|
||||
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
|
||||
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(2).TriggerClick());
|
||||
AddAssert("first object rotated 90deg around selection centre",
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
|
||||
AddAssert("second object rotated 90deg around selection centre",
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -22,9 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
|
||||
{
|
||||
private Slider slider;
|
||||
private DrawableSlider drawableObject;
|
||||
private TestSliderBlueprint blueprint;
|
||||
private Slider slider = null!;
|
||||
private DrawableSlider drawableObject = null!;
|
||||
private TestSliderBlueprint blueprint = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
@ -218,6 +216,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("tail positioned correctly",
|
||||
() => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddAssert("end drag marker positioned correctly",
|
||||
() => Precision.AlmostEquals(blueprint.TailOverlay.EndDragMarker!.ToScreenSpace(blueprint.TailOverlay.EndDragMarker.OriginPosition), drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre, 2));
|
||||
}
|
||||
|
||||
private void moveMouseToControlPoint(int index)
|
||||
@ -230,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
}
|
||||
|
||||
private void checkControlPointSelected(int index, bool selected)
|
||||
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
|
||||
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser!.Pieces[index].IsSelected.Value == selected);
|
||||
|
||||
private partial class TestSliderBlueprint : SliderSelectionBlueprint
|
||||
{
|
||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
|
||||
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
|
||||
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
public new PathControlPointVisualiser<Slider>? ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
|
||||
public TestSliderBlueprint(Slider slider)
|
||||
: base(slider)
|
||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
||||
SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false);
|
||||
SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false);
|
||||
SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false);
|
||||
SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 750);
|
||||
SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 800);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
public static class SpeedEvaluator
|
||||
{
|
||||
private const double single_spacing_threshold = 125;
|
||||
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
|
||||
private const double min_speed_bonus = 75; // ~200BPM
|
||||
private const double speed_balancing_factor = 40;
|
||||
|
||||
@ -50,16 +50,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||
|
||||
// derive speedBonus for calculation
|
||||
// speedBonus will be 1.0 for BPM < 200
|
||||
double speedBonus = 1.0;
|
||||
|
||||
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||
if (strainTime < min_speed_bonus)
|
||||
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||
|
||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
|
||||
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||
|
||||
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) * doubletapness / strainTime;
|
||||
// Cap distance at single_spacing_threshold
|
||||
distance = Math.Min(distance, single_spacing_threshold);
|
||||
|
||||
// Max distance bonus is 2 at single_spacing_threshold
|
||||
double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5);
|
||||
|
||||
// Base difficulty with all bonuses
|
||||
double difficulty = speedBonus * distanceBonus * 1000 / strainTime;
|
||||
|
||||
// Apply penalty if there's doubletappable doubles
|
||||
return difficulty * doubletapness;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 23.55;
|
||||
private double skillMultiplier => 24.963;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
||||
}
|
||||
|
||||
private double skillMultiplier => 0.052;
|
||||
private double skillMultiplier => 0.05512;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double currentStrain;
|
||||
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
|
||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
|
||||
}
|
||||
|
@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
public abstract class OsuStrainSkill : StrainSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// The default multiplier applied by <see cref="OsuStrainSkill"/> to the final difficulty value after all other calculations.
|
||||
/// May be overridden via <see cref="DifficultyMultiplier"/>.
|
||||
/// </summary>
|
||||
public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
|
||||
|
||||
/// <summary>
|
||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
||||
@ -29,11 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
protected virtual double ReducedStrainBaseline => 0.75;
|
||||
|
||||
/// <summary>
|
||||
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
|
||||
/// </summary>
|
||||
protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
|
||||
|
||||
protected OsuStrainSkill(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
@ -65,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
weight *= DecayWeight;
|
||||
}
|
||||
|
||||
return difficulty * DifficultyMultiplier;
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
@ -16,14 +16,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Speed : OsuStrainSkill
|
||||
{
|
||||
private double skillMultiplier => 1375;
|
||||
private double skillMultiplier => 1.430;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
private double currentRhythm;
|
||||
|
||||
protected override int ReducedSectionCount => 5;
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
|
||||
private readonly List<double> objectStrains = new List<double>();
|
||||
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
if (endDragMarkerContainer != null)
|
||||
{
|
||||
endDragMarkerContainer.Position = circle.Position;
|
||||
endDragMarkerContainer.Position = circle.Position + slider.StackOffset;
|
||||
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
|
||||
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
|
||||
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
|
||||
|
@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||
GridToolbox = OsuGridToolboxGroup,
|
||||
},
|
||||
new GenerateToolboxGroup(),
|
||||
FreehandSliderToolboxGroup
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -25,6 +26,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved]
|
||||
private OsuGridToolboxGroup gridToolbox { get; set; } = null!;
|
||||
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
@ -123,13 +127,43 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
// If we're flipping over the origin, we take the grid origin position from the grid toolbox.
|
||||
var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
Vector2 flipAxis = direction == Direction.Vertical ? Vector2.UnitY : Vector2.UnitX;
|
||||
|
||||
if (flipOverOrigin)
|
||||
{
|
||||
// If we're flipping over the origin, we take one of the axes of the grid.
|
||||
// Take the axis closest to the direction we want to flip over.
|
||||
switch (gridToolbox.GridType.Value)
|
||||
{
|
||||
case PositionSnapGridType.Square:
|
||||
flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 45) % 90 - 45));
|
||||
flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis;
|
||||
break;
|
||||
|
||||
case PositionSnapGridType.Triangle:
|
||||
// Hex grid has 3 axes, so you can not directly flip over one of the axes,
|
||||
// however it's still possible to achieve that flip by combining multiple flips over the other axes.
|
||||
// Angle degree range for vertical = (-120, -60]
|
||||
// Angle degree range for horizontal = [-30, 30)
|
||||
flipAxis = direction == Direction.Vertical
|
||||
? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 30) % 60 + 60))
|
||||
: GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var controlPointFlipQuad = new Quad();
|
||||
|
||||
bool didFlip = false;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position);
|
||||
|
||||
// Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered.
|
||||
flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||
|
||||
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
||||
{
|
||||
@ -142,12 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
didFlip = true;
|
||||
|
||||
foreach (var cp in slider.Path.ControlPoints)
|
||||
{
|
||||
cp.Position = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * cp.Position.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * cp.Position.Y
|
||||
);
|
||||
}
|
||||
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||
private Vector2? defaultOrigin;
|
||||
private List<Vector2>? originalConvexHull;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
@ -84,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
|
||||
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
|
||||
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
if (!OperationInProgress.Value)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
@ -94,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
scale = clampScaleToAdjustAxis(scale, adjustAxis);
|
||||
|
||||
// for the time being, allow resizing of slider paths only if the slider is
|
||||
// the only hit object selected. with a group selection, it's likely the user
|
||||
@ -102,15 +107,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var originalInfo = objectsInScale[slider];
|
||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
|
||||
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
|
||||
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation);
|
||||
|
||||
foreach (var (ho, originalState) in objectsInScale)
|
||||
{
|
||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
|
||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,14 +139,34 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
.Where(h => h is not Spinner);
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
|
||||
private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis)
|
||||
{
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.Y:
|
||||
scale.X = 1;
|
||||
break;
|
||||
|
||||
case Axes.X:
|
||||
scale.Y = 1;
|
||||
break;
|
||||
|
||||
case Axes.None:
|
||||
scale = Vector2.One;
|
||||
break;
|
||||
}
|
||||
|
||||
return scale;
|
||||
}
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0)
|
||||
{
|
||||
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
|
||||
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation);
|
||||
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
||||
}
|
||||
|
||||
@ -176,11 +201,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// </summary>
|
||||
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||
/// <param name="scale">The scale to be clamped</param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
/// <param name="axisRotation">The rotation of the axes in degrees</param>
|
||||
/// <returns>The clamped scale vector</returns>
|
||||
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
|
||||
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
if (objectsInScale == null)
|
||||
if (objectsInScale == null || adjustAxis == Axes.None)
|
||||
return scale;
|
||||
|
||||
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
@ -188,24 +215,60 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||
origin = slider.Position;
|
||||
|
||||
float cos = MathF.Cos(float.DegreesToRadians(-axisRotation));
|
||||
float sin = MathF.Sin(float.DegreesToRadians(-axisRotation));
|
||||
scale = clampScaleToAdjustAxis(scale, adjustAxis);
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||
IEnumerable<Vector2> points;
|
||||
|
||||
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||
if (axisRotation == 0)
|
||||
{
|
||||
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||
points = new[]
|
||||
{
|
||||
selectionQuad.TopLeft,
|
||||
selectionQuad.TopRight,
|
||||
selectionQuad.BottomLeft,
|
||||
selectionQuad.BottomRight
|
||||
};
|
||||
}
|
||||
else
|
||||
points = originalConvexHull!;
|
||||
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
|
||||
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
|
||||
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
|
||||
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
|
||||
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
||||
foreach (var point in points)
|
||||
{
|
||||
scale = clampToBound(scale, point, Vector2.Zero);
|
||||
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
||||
}
|
||||
|
||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
|
||||
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
|
||||
|
||||
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
||||
{
|
||||
p -= actualOrigin;
|
||||
bound -= actualOrigin;
|
||||
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
||||
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.X:
|
||||
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
|
||||
break;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -19,16 +20,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly SelectionRotationHandler rotationHandler;
|
||||
|
||||
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
|
||||
private readonly OsuGridToolboxGroup gridToolbox;
|
||||
|
||||
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.GridCentre));
|
||||
|
||||
private SliderWithTextBoxInput<float> angleInput = null!;
|
||||
private EditorRadioButtonCollection rotationOrigin = null!;
|
||||
|
||||
private RadioButton selectionCentreButton = null!;
|
||||
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
|
||||
{
|
||||
this.rotationHandler = rotationHandler;
|
||||
this.gridToolbox = gridToolbox;
|
||||
|
||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||
}
|
||||
@ -58,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = new[]
|
||||
{
|
||||
new RadioButton("Grid centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||
new RadioButton("Playfield centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
@ -93,10 +100,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
rotationInfo.BindValueChanged(rotation =>
|
||||
{
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
|
||||
});
|
||||
}
|
||||
|
||||
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
|
||||
rotation.Origin switch
|
||||
{
|
||||
RotationOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||
RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||
RotationOrigin.SelectionCentre => null,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
|
||||
};
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
@ -114,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public enum RotationOrigin
|
||||
{
|
||||
GridCentre,
|
||||
PlayfieldCentre,
|
||||
SelectionCentre
|
||||
}
|
||||
|
@ -20,21 +20,25 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly OsuSelectionScaleHandler scaleHandler;
|
||||
|
||||
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
|
||||
private readonly OsuGridToolboxGroup gridToolbox;
|
||||
|
||||
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true));
|
||||
|
||||
private SliderWithTextBoxInput<float> scaleInput = null!;
|
||||
private BindableNumber<float> scaleInputBindable = null!;
|
||||
private EditorRadioButtonCollection scaleOrigin = null!;
|
||||
|
||||
private RadioButton gridCentreButton = null!;
|
||||
private RadioButton playfieldCentreButton = null!;
|
||||
private RadioButton selectionCentreButton = null!;
|
||||
|
||||
private OsuCheckbox xCheckBox = null!;
|
||||
private OsuCheckbox yCheckBox = null!;
|
||||
|
||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
|
||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
|
||||
{
|
||||
this.scaleHandler = scaleHandler;
|
||||
this.gridToolbox = gridToolbox;
|
||||
|
||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||
}
|
||||
@ -66,6 +70,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = new[]
|
||||
{
|
||||
gridCentreButton = new RadioButton("Grid centre",
|
||||
() => setOrigin(ScaleOrigin.GridCentre),
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
@ -97,6 +104,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
},
|
||||
}
|
||||
};
|
||||
gridCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
gridCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to grid centre." : string.Empty;
|
||||
};
|
||||
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
|
||||
@ -123,19 +134,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||
gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled;
|
||||
|
||||
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||
|
||||
scaleInfo.BindValueChanged(scale =>
|
||||
{
|
||||
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
|
||||
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
|
||||
var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale);
|
||||
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue));
|
||||
});
|
||||
}
|
||||
|
||||
private void updateAxisCheckBoxesEnabled()
|
||||
{
|
||||
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
|
||||
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
||||
{
|
||||
toggleAxisAvailable(xCheckBox.Current, true);
|
||||
toggleAxisAvailable(yCheckBox.Current, true);
|
||||
@ -162,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return;
|
||||
|
||||
const float max_scale = 10;
|
||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
|
||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||
|
||||
if (!scaleInfo.Value.XAxis)
|
||||
scale.X = max_scale;
|
||||
@ -179,7 +191,18 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
updateAxisCheckBoxesEnabled();
|
||||
}
|
||||
|
||||
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
|
||||
private Vector2? getOriginPosition(PreciseScaleInfo scale) =>
|
||||
scale.Origin switch
|
||||
{
|
||||
ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||
ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||
ScaleOrigin.SelectionCentre => null,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(scale))
|
||||
};
|
||||
|
||||
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
|
||||
|
||||
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||
|
||||
private void setAxis(bool x, bool y)
|
||||
{
|
||||
@ -204,6 +227,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public enum ScaleOrigin
|
||||
{
|
||||
GridCentre,
|
||||
PlayfieldCentre,
|
||||
SelectionCentre
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
|
||||
|
||||
public OsuGridToolboxGroup GridToolbox { get; init; } = null!;
|
||||
|
||||
public TransformToolboxGroup()
|
||||
: base("transform")
|
||||
{
|
||||
@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
rotateButton = new EditorToolButton("Rotate",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||
() => new PreciseRotationPopover(RotationHandler)),
|
||||
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
||||
scaleButton = new EditorToolButton("Scale",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||
() => new PreciseScalePopover(ScaleHandler))
|
||||
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -91,20 +91,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
|
||||
}
|
||||
else
|
||||
applyDim(piece);
|
||||
}
|
||||
|
||||
void applyDim(Drawable piece)
|
||||
{
|
||||
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||
piece.FadeColour(Color4.White, 100);
|
||||
// but at the end apply the transforms now regardless of whether this is a DHO or not.
|
||||
// the above is just to ensure they don't get overwritten later.
|
||||
applyDim(piece);
|
||||
}
|
||||
|
||||
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
||||
}
|
||||
|
||||
protected override void ClearNestedHitObjects()
|
||||
{
|
||||
base.ClearNestedHitObjects();
|
||||
|
||||
// any dimmable pieces that are DHOs will be pooled separately.
|
||||
// `applyDimToDrawableHitObject` is a closure that implicitly captures `this`,
|
||||
// and because of separate pooling of parent and child objects, there is no guarantee that the pieces will be associated with `this` again on re-use.
|
||||
// therefore, clean up the subscription here to avoid crosstalk.
|
||||
// not doing so can result in the callback attempting to read things from `this` when it is in a completely bogus state (not in use or similar).
|
||||
foreach (var piece in DimmablePieces.OfType<DrawableHitObject>())
|
||||
piece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||
}
|
||||
|
||||
private void applyDim(Drawable piece)
|
||||
{
|
||||
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||
piece.FadeColour(Color4.White, 100);
|
||||
}
|
||||
|
||||
private void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
||||
|
||||
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
|
||||
|
||||
private OsuInputManager osuActionInputManager;
|
||||
|
@ -44,7 +44,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
if (player is ReplayPlayer || player is SpectatorPlayer)
|
||||
{
|
||||
PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(player.Score.Replay));
|
||||
ReplayAnalysisOverlay analysisOverlay;
|
||||
PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(player.Score.Replay));
|
||||
Overlays.Add(analysisOverlay.CreateProxy().With(p => p.Depth = float.NegativeInfinity));
|
||||
|
||||
cursorHideEnabled = Config.GetBindable<bool>(OsuRulesetSetting.ReplayCursorHideEnabled);
|
||||
|
||||
|
@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
protected override void OnApply(AnalysisFrameEntry entry)
|
||||
{
|
||||
Position = entry.Position;
|
||||
Depth = -(float)entry.LifetimeEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 1.35;
|
||||
|
||||
private const double final_multiplier = 0.0625;
|
||||
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
|
||||
private const double colour_skill_multiplier = 0.375 * final_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
|
||||
private const double difficulty_multiplier = 0.084375;
|
||||
private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier;
|
||||
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
|
||||
public override int Version => 20221107;
|
||||
|
||||
@ -83,11 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier;
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier;
|
||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
||||
double starRating = rescale(combinedRating * 1.4);
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
|
@ -633,11 +633,15 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
new object[] { "0", DateTimeOffset.Now, false },
|
||||
new object[] { "false", DateTimeOffset.MinValue, true },
|
||||
new object[] { "false", DateTimeOffset.Now, false },
|
||||
new object[] { "no", DateTimeOffset.MinValue, true },
|
||||
new object[] { "no", DateTimeOffset.Now, false },
|
||||
|
||||
new object[] { "1", DateTimeOffset.MinValue, false },
|
||||
new object[] { "1", DateTimeOffset.Now, true },
|
||||
new object[] { "true", DateTimeOffset.MinValue, false },
|
||||
new object[] { "true", DateTimeOffset.Now, true },
|
||||
new object[] { "yes", DateTimeOffset.MinValue, false },
|
||||
new object[] { "yes", DateTimeOffset.Now, true },
|
||||
};
|
||||
|
||||
[Test]
|
||||
|
33
osu.Game.Tests/Utils/GeometryUtilsTest.cs
Normal file
33
osu.Game.Tests/Utils/GeometryUtilsTest.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Utils
|
||||
{
|
||||
[TestFixture]
|
||||
public class GeometryUtilsTest
|
||||
{
|
||||
[TestCase(new int[] { }, new int[] { })]
|
||||
[TestCase(new[] { 0, 0 }, new[] { 0, 0 })]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, new[] { 0, 0, 4, 10, 2, -1 })]
|
||||
public void TestConvexHull(int[] values, int[] expected)
|
||||
{
|
||||
var points = new Vector2[values.Length / 2];
|
||||
for (int i = 0; i < values.Length; i += 2)
|
||||
points[i / 2] = new Vector2(values[i], values[i + 1]);
|
||||
|
||||
var expectedPoints = new Vector2[expected.Length / 2];
|
||||
for (int i = 0; i < expected.Length; i += 2)
|
||||
expectedPoints[i / 2] = new Vector2(expected[i], expected[i + 1]);
|
||||
|
||||
var hull = GeometryUtils.GetConvexHull(points);
|
||||
|
||||
Assert.That(hull, Is.EquivalentTo(expectedPoints));
|
||||
}
|
||||
}
|
||||
}
|
@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@ -136,6 +137,59 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCalibrationFromNonZeroWithImmediateReferenceScore()
|
||||
{
|
||||
const double average_error = -4.5;
|
||||
const double initial_offset = -2;
|
||||
|
||||
AddStep("Set beatmap offset non-neutral", () => Realm.Write(r =>
|
||||
{
|
||||
r.Add(new BeatmapInfo
|
||||
{
|
||||
ID = Beatmap.Value.BeatmapInfo.ID,
|
||||
Ruleset = Beatmap.Value.BeatmapInfo.Ruleset,
|
||||
UserSettings =
|
||||
{
|
||||
Offset = initial_offset,
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
AddStep("Create control with preloaded reference score", () =>
|
||||
{
|
||||
Child = new PlayerSettingsGroup("Some settings")
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
offsetControl = new BeatmapOffsetControl
|
||||
{
|
||||
ReferenceScore =
|
||||
{
|
||||
Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
|
||||
AddAssert("Offset is adjusted", () => offsetControl.Current.Value, () => Is.EqualTo(initial_offset - average_error));
|
||||
|
||||
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
|
||||
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
|
||||
AddStep("Clean up beatmap", () => Realm.Write(r => r.RemoveAll<BeatmapInfo>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCalibrationNoChange()
|
||||
{
|
||||
|
@ -8,7 +8,9 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
@ -26,6 +28,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -177,6 +180,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyFailStillImports()
|
||||
{
|
||||
prepareTestAPI(true);
|
||||
|
||||
createPlayerTest(true);
|
||||
|
||||
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
|
||||
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
|
||||
AddStep("attempt import", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Player.ChildrenOfType<SaveFailedScoreButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("wait for import to start", () => Player.ScoreImportStarted);
|
||||
AddStep("allow import", () => Player.AllowImportCompletion.Release());
|
||||
|
||||
AddUntilStep("import completed", () => Player.ImportedScore, () => Is.Not.Null);
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore, () => Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSubmissionOnFail()
|
||||
{
|
||||
@ -378,6 +405,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public SemaphoreSlim AllowImportCompletion { get; }
|
||||
public Score ImportedScore { get; private set; }
|
||||
|
||||
public new FailOverlay FailOverlay => base.FailOverlay;
|
||||
|
||||
public FakeImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
|
||||
: base(allowPause, showResults, pauseOnFocusLost)
|
||||
{
|
||||
|
@ -440,8 +440,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddStep("import old classic skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"classic-layout-version-0.osk").SkinInfo);
|
||||
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
|
||||
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<LegacyComboCounter>().Any());
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
|
||||
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Any());
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
|
||||
AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyDefaultComboCounter
|
||||
{
|
||||
@ -454,8 +454,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault());
|
||||
AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin);
|
||||
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
|
||||
AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
|
||||
AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
}
|
||||
|
||||
private Skin importSkinFromArchives(string filename)
|
||||
|
@ -144,6 +144,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
exitViaEscapeAndConfirm();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnterGameplayWhileFilteringToNoSelection()
|
||||
{
|
||||
TestPlaySongSelect songSelect = null;
|
||||
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("force selection", () =>
|
||||
{
|
||||
songSelect.FinaliseSelection();
|
||||
songSelect.FilterControl.CurrentTextSearch.Value = "test";
|
||||
});
|
||||
|
||||
AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen());
|
||||
AddStep("return to song select", () => songSelect.MakeCurrent());
|
||||
|
||||
AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSongSelectBackActionHandling()
|
||||
{
|
||||
|
@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Test]
|
||||
public void TestPlayCountRankingTier()
|
||||
{
|
||||
AddAssert("1 before silver", () => DailyChallengeStatsDisplay.TierForPlayCount(30) == RankingTier.Bronze);
|
||||
AddAssert("first silver", () => DailyChallengeStatsDisplay.TierForPlayCount(31) == RankingTier.Silver);
|
||||
AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze);
|
||||
AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -520,6 +520,17 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
waitForSelection(set_count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifficultiesSplitOutOnLoad()
|
||||
{
|
||||
loadBeatmaps(new List<BeatmapSetInfo> { TestResources.CreateTestBeatmapSetInfo(diff_count) }, () => new FilterCriteria
|
||||
{
|
||||
Sort = SortMode.Difficulty,
|
||||
});
|
||||
|
||||
checkVisibleItemCount(false, 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddRemoveDifficultySort()
|
||||
{
|
||||
@ -1120,6 +1131,32 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRetainsSelectionFromDifficultySort()
|
||||
{
|
||||
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
||||
|
||||
AddStep("Populate beatmap sets", () =>
|
||||
{
|
||||
manySets.Clear();
|
||||
|
||||
for (int i = 1; i <= 50; i++)
|
||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||
});
|
||||
|
||||
loadBeatmaps(manySets);
|
||||
|
||||
BeatmapInfo chosenBeatmap = null!;
|
||||
AddStep("select given beatmap", () => carousel.SelectBeatmap(chosenBeatmap = manySets[20].Beatmaps[0]));
|
||||
AddUntilStep("selection changed", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
AddStep("sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||
AddAssert("selection retained", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
AddStep("sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }));
|
||||
AddAssert("selection retained", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilteringByUserStarDifficulty()
|
||||
{
|
||||
|
@ -1,11 +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.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
@ -25,7 +27,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding(10),
|
||||
@ -53,9 +58,45 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
PlaceholderText = "Mine is 42!",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
Current = { Disabled = true },
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Instantaneous slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Non-instantaneous slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
Instantaneous = false,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@ -78,7 +79,7 @@ namespace osu.Game.Beatmaps
|
||||
// cached database exists on disk.
|
||||
&& storage.Exists(cache_database_name);
|
||||
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, [NotNullWhen(true)] out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
@ -98,7 +99,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))))
|
||||
using (var db = getConnection())
|
||||
{
|
||||
db.Open();
|
||||
|
||||
@ -125,6 +126,9 @@ namespace osu.Game.Beatmaps
|
||||
return false;
|
||||
}
|
||||
|
||||
private SqliteConnection getConnection() =>
|
||||
new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true)));
|
||||
|
||||
private void prepareLocalCache()
|
||||
{
|
||||
bool isRefetch = storage.Exists(cache_database_name);
|
||||
@ -191,6 +195,15 @@ namespace osu.Game.Beatmaps
|
||||
});
|
||||
}
|
||||
|
||||
public int GetCacheVersion()
|
||||
{
|
||||
using (var connection = getConnection())
|
||||
{
|
||||
connection.Open();
|
||||
return getCacheVersion(connection);
|
||||
}
|
||||
}
|
||||
|
||||
private int getCacheVersion(SqliteConnection connection)
|
||||
{
|
||||
using (var cmd = connection.CreateCommand())
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -11,6 +12,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
@ -61,6 +63,9 @@ namespace osu.Game.Database
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Storage storage { get; set; } = null!;
|
||||
|
||||
protected virtual int TimeToSleepDuringGameplay => 30000;
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -78,6 +83,7 @@ namespace osu.Game.Database
|
||||
processScoresWithMissingStatistics();
|
||||
convertLegacyTotalScoreToStandardised();
|
||||
upgradeScoreRanks();
|
||||
backpopulateMissingSubmissionAndRankDates();
|
||||
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception?.InnerException is ObjectDisposedException)
|
||||
@ -443,6 +449,104 @@ namespace osu.Game.Database
|
||||
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
|
||||
}
|
||||
|
||||
private void backpopulateMissingSubmissionAndRankDates()
|
||||
{
|
||||
var localMetadataSource = new LocalCachedBeatmapMetadataSource(storage);
|
||||
|
||||
if (!localMetadataSource.Available)
|
||||
{
|
||||
Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (localMetadataSource.GetCacheVersion() < 2)
|
||||
{
|
||||
Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is too old.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Error when trying to query version of local metadata cache: {ex}");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log("Querying for beatmap sets that contain missing submission/rank date...");
|
||||
|
||||
HashSet<Guid> beatmapSetIds = realmAccess.Run(r => new HashSet<Guid>(
|
||||
r.All<BeatmapSetInfo>()
|
||||
.Where(b => b.StatusInt > 0 && (b.DateRanked == null || b.DateSubmitted == null))
|
||||
.AsEnumerable()
|
||||
.Select(b => b.ID)));
|
||||
|
||||
if (beatmapSetIds.Count == 0)
|
||||
return;
|
||||
|
||||
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date.");
|
||||
|
||||
var notification = showProgressNotification(beatmapSetIds.Count, "Populating missing submission and rank dates", "beatmap sets now have correct submission and rank dates.");
|
||||
|
||||
int processedCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
foreach (var id in beatmapSetIds)
|
||||
{
|
||||
if (notification?.State == ProgressNotificationState.Cancelled)
|
||||
break;
|
||||
|
||||
updateNotificationProgress(notification, processedCount, beatmapSetIds.Count);
|
||||
|
||||
sleepIfRequired();
|
||||
|
||||
try
|
||||
{
|
||||
// Can't use async overload because we're not on the update thread.
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
bool succeeded = realmAccess.Write(r =>
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = r.Find<BeatmapSetInfo>(id)!;
|
||||
|
||||
// we want any ranked representative of the set.
|
||||
// the reason for checking ranked status of the difficulty is that it can be locally modified,
|
||||
// at which point the lookup will fail - but there might still be another unmodified difficulty on which it will work.
|
||||
if (beatmapSet.Beatmaps.FirstOrDefault(b => b.Status >= BeatmapOnlineStatus.Ranked) is not BeatmapInfo beatmap)
|
||||
return false;
|
||||
|
||||
bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result);
|
||||
|
||||
if (lookupSucceeded)
|
||||
{
|
||||
Debug.Assert(result != null);
|
||||
beatmapSet.DateRanked = result.DateRanked;
|
||||
beatmapSet.DateSubmitted = result.DateSubmitted;
|
||||
return true;
|
||||
}
|
||||
|
||||
Logger.Log($"Could not find {beatmapSet.GetDisplayString()} in local cache while backpopulating missing submission/rank date");
|
||||
return false;
|
||||
});
|
||||
|
||||
if (succeeded)
|
||||
++processedCount;
|
||||
else
|
||||
++failedCount;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Failed to update ranked/submitted dates for beatmap set {id}: {e}");
|
||||
++failedCount;
|
||||
}
|
||||
}
|
||||
|
||||
completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount);
|
||||
}
|
||||
|
||||
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
|
||||
{
|
||||
if (notification == null)
|
||||
|
@ -195,6 +195,7 @@ namespace osu.Game.Database
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||
|
||||
// Consider using hard links here to make this instant.
|
||||
using (var inStream = Files.Storage.GetStream(sourcePath))
|
||||
using (var outStream = File.Create(destinationPath))
|
||||
await inStream.CopyToAsync(outStream).ConfigureAwait(false);
|
||||
|
@ -75,7 +75,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio)
|
||||
{
|
||||
BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
||||
BackgroundColour = colourProvider?.Background5 ?? Color4.Black;
|
||||
HoverColour = colourProvider?.Light4 ?? colours.PinkDarker;
|
||||
SelectionColour = colourProvider?.Background3 ?? colours.PinkDarker.Opacity(0.5f);
|
||||
|
||||
@ -397,7 +397,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
bool hovered = Enabled.Value && IsHovered;
|
||||
var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker;
|
||||
var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
||||
var unhoveredColour = colourProvider?.Background5 ?? Color4.Black;
|
||||
|
||||
Colour = Color4.White;
|
||||
Alpha = Enabled.Value ? 1 : 0.3f;
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true);
|
||||
CurrentNumber.BindValueChanged(current => TooltipText = GetDisplayableValue(current.NewValue), true);
|
||||
}
|
||||
|
||||
protected override void OnUserChange(T value)
|
||||
@ -55,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
playSample(value);
|
||||
|
||||
TooltipText = getTooltipText(value);
|
||||
TooltipText = GetDisplayableValue(value);
|
||||
}
|
||||
|
||||
private void playSample(T value)
|
||||
@ -83,7 +83,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
channel.Play();
|
||||
}
|
||||
|
||||
private LocalisableString getTooltipText(T value)
|
||||
public LocalisableString GetDisplayableValue(T value)
|
||||
{
|
||||
if (CurrentNumber.IsInteger)
|
||||
return int.CreateTruncating(value).ToString("N0");
|
||||
|
161
osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs
Normal file
161
osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs
Normal file
@ -0,0 +1,161 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue<bool>
|
||||
{
|
||||
public Bindable<bool> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Caption describing this slider bar, displayed on top of the controls.
|
||||
/// </summary>
|
||||
public LocalisableString Caption { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
|
||||
/// </summary>
|
||||
public LocalisableString HintText { get; init; }
|
||||
|
||||
private Box background = null!;
|
||||
private FormFieldCaption caption = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
private Nub checkbox = null!;
|
||||
|
||||
private Sample? sampleChecked;
|
||||
private Sample? sampleUnchecked;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 50;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
CornerExponent = 2.5f;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(9),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
caption = new FormFieldCaption
|
||||
{
|
||||
Caption = Caption,
|
||||
TooltipText = HintText,
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
},
|
||||
checkbox = new Nub
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Current = Current,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
sampleChecked = audio.Samples.Get(@"UI/check-on");
|
||||
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
current.BindValueChanged(_ =>
|
||||
{
|
||||
updateState();
|
||||
playSamples();
|
||||
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
|
||||
});
|
||||
current.BindDisabledChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
private void playSamples()
|
||||
{
|
||||
if (Current.Value)
|
||||
sampleChecked?.Play();
|
||||
else
|
||||
sampleUnchecked?.Play();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (!Current.Disabled)
|
||||
Current.Value = !Current.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5;
|
||||
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
|
||||
checkbox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
|
||||
text.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
|
||||
|
||||
text.Text = Current.Value ? CommonStrings.Enabled : CommonStrings.Disabled;
|
||||
|
||||
if (!Current.Disabled)
|
||||
{
|
||||
BorderThickness = IsHovered ? 2 : 0;
|
||||
|
||||
if (IsHovered)
|
||||
BorderColour = colourProvider.Light4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
374
osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs
Normal file
374
osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs
Normal file
@ -0,0 +1,374 @@
|
||||
// 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.Globalization;
|
||||
using System.Numerics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class FormSliderBar<T> : CompositeDrawable, IHasCurrentValue<T>
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
public Bindable<T> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private bool instantaneous = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
|
||||
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
|
||||
/// </summary>
|
||||
public bool Instantaneous
|
||||
{
|
||||
get => instantaneous;
|
||||
set
|
||||
{
|
||||
instantaneous = value;
|
||||
|
||||
if (slider.IsNotNull())
|
||||
slider.TransferValueOnCommit = !instantaneous;
|
||||
}
|
||||
}
|
||||
|
||||
private CompositeDrawable? tabbableContentContainer;
|
||||
|
||||
public CompositeDrawable? TabbableContentContainer
|
||||
{
|
||||
set
|
||||
{
|
||||
tabbableContentContainer = value;
|
||||
|
||||
if (textBox.IsNotNull())
|
||||
textBox.TabbableContentContainer = tabbableContentContainer;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Caption describing this slider bar, displayed on top of the controls.
|
||||
/// </summary>
|
||||
public LocalisableString Caption { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
|
||||
/// </summary>
|
||||
public LocalisableString HintText { get; init; }
|
||||
|
||||
private Box background = null!;
|
||||
private Box flashLayer = null!;
|
||||
private FormTextBox.InnerTextBox textBox = null!;
|
||||
private Slider slider = null!;
|
||||
private FormFieldCaption caption = null!;
|
||||
private IFocusManager focusManager = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 50;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
flashLayer = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.Transparent,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(9),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
caption = new FormFieldCaption
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Caption = Caption,
|
||||
TooltipText = HintText,
|
||||
},
|
||||
textBox = new FormNumberBox.InnerNumberBox
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
CommitOnFocusLost = true,
|
||||
SelectAllOnFocus = true,
|
||||
AllowDecimals = true,
|
||||
OnInputError = () =>
|
||||
{
|
||||
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);
|
||||
flashLayer.FadeOutFromOne(200, Easing.OutQuint);
|
||||
},
|
||||
TabbableContentContainer = tabbableContentContainer,
|
||||
},
|
||||
slider = new Slider
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
Current = Current,
|
||||
TransferValueOnCommit = !instantaneous,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
focusManager = GetContainingFocusManager()!;
|
||||
|
||||
textBox.Focused.BindValueChanged(_ => updateState());
|
||||
textBox.OnCommit += textCommitted;
|
||||
textBox.Current.BindValueChanged(textChanged);
|
||||
|
||||
slider.IsDragging.BindValueChanged(_ => updateState());
|
||||
|
||||
current.BindValueChanged(_ =>
|
||||
{
|
||||
updateState();
|
||||
updateTextBoxFromSlider();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private bool updatingFromTextBox;
|
||||
|
||||
private void textChanged(ValueChangedEvent<string> change)
|
||||
{
|
||||
if (!instantaneous) return;
|
||||
|
||||
tryUpdateSliderFromTextBox();
|
||||
}
|
||||
|
||||
private void textCommitted(TextBox t, bool isNew)
|
||||
{
|
||||
tryUpdateSliderFromTextBox();
|
||||
|
||||
// If the attempted update above failed, restore text box to match the slider.
|
||||
Current.TriggerChange();
|
||||
|
||||
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
|
||||
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void tryUpdateSliderFromTextBox()
|
||||
{
|
||||
updatingFromTextBox = true;
|
||||
|
||||
try
|
||||
{
|
||||
switch (Current)
|
||||
{
|
||||
case Bindable<int> bindableInt:
|
||||
bindableInt.Value = int.Parse(textBox.Current.Value);
|
||||
break;
|
||||
|
||||
case Bindable<double> bindableDouble:
|
||||
bindableDouble.Value = double.Parse(textBox.Current.Value);
|
||||
break;
|
||||
|
||||
default:
|
||||
Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore parsing failures.
|
||||
// sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
|
||||
}
|
||||
|
||||
updatingFromTextBox = false;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
focusManager.ChangeFocus(textBox);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
textBox.Alpha = 1;
|
||||
|
||||
background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5;
|
||||
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
|
||||
textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
|
||||
|
||||
BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0;
|
||||
BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4;
|
||||
|
||||
if (textBox.Focused.Value)
|
||||
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
|
||||
else if (IsHovered || slider.IsDragging.Value)
|
||||
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
|
||||
else
|
||||
background.Colour = colourProvider.Background5;
|
||||
}
|
||||
|
||||
private void updateTextBoxFromSlider()
|
||||
{
|
||||
if (updatingFromTextBox) return;
|
||||
|
||||
textBox.Text = slider.GetDisplayableValue(Current.Value).ToString();
|
||||
}
|
||||
|
||||
private partial class Slider : OsuSliderBar<T>
|
||||
{
|
||||
public BindableBool IsDragging { get; set; } = new BindableBool();
|
||||
|
||||
private Box leftBox = null!;
|
||||
private Box rightBox = null!;
|
||||
private Circle nub = null!;
|
||||
private const float nub_width = 10;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Height = 40;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
RangePadding = nub_width / 2;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
leftBox = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
rightBox = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = RangePadding, },
|
||||
Child = nub = new Circle
|
||||
{
|
||||
Width = nub_width,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Origin = Anchor.TopCentre,
|
||||
}
|
||||
},
|
||||
new HoverClickSounds()
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
leftBox.Width = Math.Clamp(RangePadding + nub.DrawPosition.X, 0, Math.Max(0, DrawWidth)) / DrawWidth;
|
||||
rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
bool dragging = base.OnDragStart(e);
|
||||
IsDragging.Value = dragging;
|
||||
updateState();
|
||||
return dragging;
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
base.OnDragEnd(e);
|
||||
IsDragging.Value = false;
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
rightBox.Colour = colourProvider.Background6;
|
||||
leftBox.Colour = IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2;
|
||||
nub.Colour = IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
|
||||
}
|
||||
|
||||
protected override void UpdateValue(float value)
|
||||
{
|
||||
nub.MoveToX(value, 200, Easing.OutPow10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -59,8 +59,19 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
private readonly BindableWithCurrent<string> current = new BindableWithCurrent<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Caption describing this slider bar, displayed on top of the controls.
|
||||
/// </summary>
|
||||
public LocalisableString Caption { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
|
||||
/// </summary>
|
||||
public LocalisableString HintText { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Text displayed in the text box when its contents are empty.
|
||||
/// </summary>
|
||||
public LocalisableString PlaceholderText { get; init; }
|
||||
|
||||
private Box background = null!;
|
||||
@ -122,7 +133,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
if (!current.Disabled && !ReadOnly)
|
||||
{
|
||||
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2);
|
||||
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
|
||||
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
|
||||
}
|
||||
};
|
||||
|
@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
title.Clear();
|
||||
artist.Clear();
|
||||
|
||||
title.AddLink(titleText, LinkAction.SearchBeatmapSet, titleText);
|
||||
title.AddLink(titleText, LinkAction.SearchBeatmapSet, $@"title=""""{titleText}""""");
|
||||
|
||||
title.AddArbitraryDrawable(Empty().With(d => d.Width = 5));
|
||||
title.AddArbitraryDrawable(externalLink = new ExternalLinkButton());
|
||||
@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
title.AddArbitraryDrawable(new SpotlightBeatmapBadge());
|
||||
}
|
||||
|
||||
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, artistText);
|
||||
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, $@"artist=""""{artistText}""""");
|
||||
|
||||
if (setInfo.NewValue.TrackId != null)
|
||||
{
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
@ -14,7 +13,6 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
@ -99,7 +97,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (User.Value == null || User.Value.Ruleset.OnlineID != 0)
|
||||
if (User.Value == null)
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
@ -107,19 +105,20 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
|
||||
APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics;
|
||||
|
||||
if (stats.PlayCount == 0)
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0"));
|
||||
dailyPlayCount.Colour = colours.ForRankingTier(TierForPlayCount(stats.PlayCount));
|
||||
dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount));
|
||||
|
||||
TooltipContent = new DailyChallengeTooltipData(colourProvider, stats);
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
// Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count.
|
||||
// This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would
|
||||
// get truncated to 10 with an integer division and show a lower tier.
|
||||
public static RankingTier TierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily((int)Math.Ceiling(playCount / 3.0d));
|
||||
|
||||
public ITooltip<DailyChallengeTooltipData> GetCustomTooltip() => new DailyChallengeStatsTooltip();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
@ -26,6 +27,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
private StreakPiece currentDaily = null!;
|
||||
private StreakPiece currentWeekly = null!;
|
||||
private StreakPiece totalParticipation = null!;
|
||||
private StatisticsPiece bestDaily = null!;
|
||||
private StatisticsPiece bestWeekly = null!;
|
||||
private StatisticsPiece topTen = null!;
|
||||
@ -80,6 +82,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
Spacing = new Vector2(30f),
|
||||
Children = new[]
|
||||
{
|
||||
totalParticipation = new StreakPiece(UsersStrings.ShowDailyChallengePlaycount),
|
||||
currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent),
|
||||
currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent),
|
||||
}
|
||||
@ -113,6 +116,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
background.Colour = colourProvider.Background4;
|
||||
topBackground.Colour = colourProvider.Background5;
|
||||
|
||||
totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0"));
|
||||
totalParticipation.ValueColour = colours.ForRankingTier(TierForPlayCount(statistics.PlayCount));
|
||||
|
||||
currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0"));
|
||||
currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent));
|
||||
|
||||
@ -132,7 +138,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
topFifty.ValueColour = colourProvider.Content2;
|
||||
}
|
||||
|
||||
// reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43
|
||||
// reference: https://github.com/ppy/osu-web/blob/adf1e94754ba9625b85eba795f4a310caf169eec/resources/js/profile-page/daily-challenge.tsx#L13-L47
|
||||
|
||||
// Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count.
|
||||
// This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would
|
||||
// get truncated to 10 with an integer division and show a lower tier.
|
||||
public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Ceiling(playCount / 3.0d));
|
||||
|
||||
public static RankingTier TierForDaily(int daily)
|
||||
{
|
||||
if (daily > 360)
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
isFlippedY = false;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
@ -40,9 +40,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
|
||||
private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!;
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
playfield.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
|
||||
playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
|
||||
@ -52,7 +54,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
var playfieldSize = drawableRuleset.Playfield.DrawSize;
|
||||
float minSide = MathF.Min(playfieldSize.X, playfieldSize.Y);
|
||||
float maxSide = MathF.Max(playfieldSize.X, playfieldSize.Y);
|
||||
drawableRuleset.Playfield.Scale = new Vector2(minSide / maxSide);
|
||||
|
||||
playfieldAdjustmentContainer = drawableRuleset.PlayfieldAdjustmentContainer;
|
||||
playfieldAdjustmentContainer.Scale = new Vector2(minSide / maxSide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,8 +83,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
flashlight.RelativeSizeAxes = Axes.Both;
|
||||
flashlight.Colour = Color4.Black;
|
||||
// Flashlight mods should always draw above any other mod adding overlays.
|
||||
flashlight.Depth = float.MinValue;
|
||||
|
||||
flashlight.Combo.BindTo(Combo);
|
||||
flashlight.GetPlayfieldScale = () => drawableRuleset.Playfield.Scale;
|
||||
@ -95,6 +93,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
// workaround for 1px gaps on the edges of the playfield which would sometimes show with "gameplay" screen scaling active.
|
||||
Padding = new MarginPadding(-1),
|
||||
Child = flashlight,
|
||||
// Flashlight mods should always draw above any other mod adding overlays.
|
||||
// NegativeInfinity is not used to allow one more thing drawn on top (used in replay analysis overlay in osu!).
|
||||
Depth = float.MinValue,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,10 +52,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
/// <param name="axisRotation">The rotation of the axes in degrees.</param>
|
||||
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
Begin();
|
||||
Update(scale, origin, adjustAxis);
|
||||
Update(scale, origin, adjustAxis, axisRotation);
|
||||
Commit();
|
||||
}
|
||||
|
||||
@ -91,7 +92,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
/// <param name="axisRotation">The rotation of the axes in degrees.</param>
|
||||
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -7,17 +7,21 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public partial class ControlPointList : CompositeDrawable
|
||||
{
|
||||
private ControlPointTable table = null!;
|
||||
private Container controls = null!;
|
||||
private OsuButton deleteButton = null!;
|
||||
private RoundedButton addButton = null!;
|
||||
|
||||
@ -31,63 +35,77 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
const float margins = 10;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new ControlPointTable
|
||||
table = new ControlPointTable
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, },
|
||||
},
|
||||
new FillFlowContainer
|
||||
controls = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding(margins),
|
||||
Spacing = new Vector2(5),
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RoundedButton
|
||||
new Box
|
||||
{
|
||||
Text = "Select closest to current time",
|
||||
Action = goToCurrentGroup,
|
||||
Size = new Vector2(220, 30),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background2,
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding(margins),
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
deleteButton = new RoundedButton
|
||||
new FillFlowContainer
|
||||
{
|
||||
Text = "-",
|
||||
Size = new Vector2(30, 30),
|
||||
Action = delete,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
BackgroundColour = colours.Red3,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Padding = new MarginPadding { Left = margins, Vertical = margins, },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RoundedButton
|
||||
{
|
||||
Text = "Select closest to current time",
|
||||
Action = goToCurrentGroup,
|
||||
Size = new Vector2(220, 30),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
},
|
||||
addButton = new RoundedButton
|
||||
new FillFlowContainer
|
||||
{
|
||||
Action = addNew,
|
||||
Size = new Vector2(160, 30),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding { Right = margins, Vertical = margins, },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
deleteButton = new RoundedButton
|
||||
{
|
||||
Text = "-",
|
||||
Size = new Vector2(30, 30),
|
||||
Action = delete,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
BackgroundColour = colours.Red3,
|
||||
},
|
||||
addButton = new RoundedButton
|
||||
{
|
||||
Action = addNew,
|
||||
Size = new Vector2(160, 30),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -119,6 +137,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
base.Update();
|
||||
|
||||
addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
|
||||
table.Padding = new MarginPadding { Bottom = controls.DrawHeight };
|
||||
}
|
||||
|
||||
private void goToCurrentGroup()
|
||||
|
@ -28,6 +28,12 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public BindableList<ControlPointGroup> Groups { get; } = new BindableList<ControlPointGroup>();
|
||||
|
||||
public new MarginPadding Padding
|
||||
{
|
||||
get => base.Padding;
|
||||
set => base.Padding = value;
|
||||
}
|
||||
|
||||
[Cached]
|
||||
private Bindable<TimingControlPoint?> activeTimingPoint { get; } = new Bindable<TimingControlPoint?>();
|
||||
|
||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
isRebinding = true;
|
||||
|
||||
kiai.Current = newEffectPoint.KiaiModeBindable;
|
||||
scrollSpeedSlider.Current = new BindableDouble
|
||||
scrollSpeedSlider.Current = new BindableDouble(1)
|
||||
{
|
||||
MinValue = 0.01,
|
||||
MaxValue = 10,
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -25,13 +26,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGame? game)
|
||||
private void load(OsuGame? game, SessionStatics statics)
|
||||
{
|
||||
Text = DailyChallengeStrings.ChallengeLiveNotification;
|
||||
Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!));
|
||||
Activated = () =>
|
||||
{
|
||||
game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]);
|
||||
if (statics.Get<bool>(Static.DailyChallengeIntroPlayed))
|
||||
game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]);
|
||||
else
|
||||
game?.PerformFromScreen(s => s.Push(new DailyChallengeIntro(room)), [typeof(MainMenu)]);
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
@ -39,14 +39,23 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private IBindable<APIUser>? apiUser;
|
||||
|
||||
private readonly Container cornerContainer;
|
||||
|
||||
public PlayerAvatar()
|
||||
{
|
||||
Size = new Vector2(default_size);
|
||||
|
||||
InternalChild = avatar = new UpdateableAvatar(isInteractive: false)
|
||||
InternalChild = cornerContainer = new Container
|
||||
{
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true
|
||||
Child = avatar = new UpdateableAvatar(isInteractive: false)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fill,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,7 +75,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
CornerRadius.BindValueChanged(e => avatar.CornerRadius = e.NewValue * default_size, true);
|
||||
CornerRadius.BindValueChanged(e => cornerContainer.CornerRadius = e.NewValue * default_size, true);
|
||||
}
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
@ -104,8 +104,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ReferenceScore.BindValueChanged(scoreChanged, true);
|
||||
|
||||
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
|
||||
settings => settings.Offset,
|
||||
@ -124,6 +122,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
});
|
||||
|
||||
Current.BindValueChanged(currentChanged);
|
||||
ReferenceScore.BindValueChanged(scoreChanged, true);
|
||||
}
|
||||
|
||||
private void currentChanged(ValueChangedEvent<double> offset)
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -16,6 +17,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -26,7 +28,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public partial class SkipOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
|
||||
public partial class SkipOverlay : BeatSyncedContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
/// <summary>
|
||||
/// The total number of successful skips performed by this overlay.
|
||||
@ -36,10 +38,9 @@ namespace osu.Game.Screens.Play
|
||||
private readonly double startTime;
|
||||
|
||||
public Action RequestSkip;
|
||||
|
||||
private Button button;
|
||||
private ButtonContainer buttonContainer;
|
||||
private Box remainingTimeBox;
|
||||
private Circle remainingTimeBox;
|
||||
|
||||
private FadeContainer fadeContainer;
|
||||
private double displayTime;
|
||||
@ -51,7 +52,6 @@ namespace osu.Game.Screens.Play
|
||||
private IGameplayClock gameplayClock { get; set; }
|
||||
|
||||
internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
/// <summary>
|
||||
@ -87,13 +87,13 @@ namespace osu.Game.Screens.Play
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
remainingTimeBox = new Box
|
||||
remainingTimeBox = new Circle
|
||||
{
|
||||
Height = 5,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Colour = colours.Yellow,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = colours.Yellow,
|
||||
RelativeSizeAxes = Axes.X
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,6 +210,18 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||
|
||||
if (fadeOutBeginTime <= gameplayClock.CurrentTime)
|
||||
return;
|
||||
|
||||
float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime);
|
||||
float newWidth = 1 - Math.Clamp(progress, 0, 1);
|
||||
remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 2, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public partial class FadeContainer : Container, IStateful<Visibility>
|
||||
{
|
||||
[CanBeNull]
|
||||
|
@ -274,6 +274,16 @@ namespace osu.Game.Screens.Play
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// if the user never hit anything, this score should not be counted in any way.
|
||||
if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0))
|
||||
{
|
||||
Logger.Log("No hits registered, skipping score submission");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// mind the timing of this.
|
||||
// once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background,
|
||||
// so all exceptional circumstances that would disallow submission must be handled above.
|
||||
lock (scoreSubmissionLock)
|
||||
{
|
||||
if (scoreSubmissionSource != null)
|
||||
@ -282,10 +292,6 @@ namespace osu.Game.Screens.Play
|
||||
scoreSubmissionSource = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
// if the user never hit anything, this score should not be counted in any way.
|
||||
if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0))
|
||||
return Task.CompletedTask;
|
||||
|
||||
Logger.Log($"Beginning score submission (token:{token.Value})...");
|
||||
var request = CreateSubmissionRequest(score, token.Value);
|
||||
|
||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Screens.Ranking
|
||||
new OsuMenu(Direction.Vertical, true)
|
||||
{
|
||||
Items = items,
|
||||
MaxHeight = 375,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -76,12 +76,13 @@ namespace osu.Game.Screens.Ranking
|
||||
};
|
||||
beatmapSetRequest.Failure += e =>
|
||||
{
|
||||
Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}");
|
||||
Logger.Log($"Favourite button failed to fetch beatmap info: {e}", LoggingTarget.Network);
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
loading.Hide();
|
||||
Enabled.Value = false;
|
||||
TooltipText = "this beatmap cannot be favourited";
|
||||
});
|
||||
};
|
||||
api.Queue(beatmapSetRequest);
|
||||
|
@ -137,11 +137,13 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void loadNewRoot()
|
||||
{
|
||||
beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
|
||||
|
||||
// Ensure no changes are made to the list while we are initialising items.
|
||||
// We'll catch up on changes via subscriptions anyway.
|
||||
BeatmapSetInfo[] loadableSets = detachedBeatmapSets!.ToArray();
|
||||
|
||||
if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet))
|
||||
if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet, EqualityComparer<BeatmapSetInfo>.Default))
|
||||
selectedBeatmapSet = null;
|
||||
|
||||
var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo;
|
||||
@ -704,7 +706,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private bool beatmapsSplitOut;
|
||||
|
||||
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
|
||||
private void applyActiveCriteria(bool debounce)
|
||||
{
|
||||
PendingFilter?.Cancel();
|
||||
PendingFilter = null;
|
||||
@ -726,7 +728,6 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut)
|
||||
{
|
||||
beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
|
||||
loadNewRoot();
|
||||
return;
|
||||
}
|
||||
@ -734,8 +735,7 @@ namespace osu.Game.Screens.Select
|
||||
root.Filter(activeCriteria);
|
||||
itemsCache.Invalidate();
|
||||
|
||||
if (alwaysResetScrollPosition || !Scroll.UserScrolling)
|
||||
ScrollToSelected(true);
|
||||
ScrollToSelected(true);
|
||||
|
||||
FilterApplied?.Invoke();
|
||||
}
|
||||
@ -1035,7 +1035,7 @@ namespace osu.Game.Screens.Select
|
||||
itemsCache.Validate();
|
||||
|
||||
// update and let external consumers know about selection loss.
|
||||
if (BeatmapSetsLoaded)
|
||||
if (BeatmapSetsLoaded && AllowSelection)
|
||||
{
|
||||
bool selectionLost = selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected;
|
||||
|
||||
|
@ -159,10 +159,12 @@ namespace osu.Game.Screens.Select
|
||||
switch (value)
|
||||
{
|
||||
case "1":
|
||||
case "yes":
|
||||
result = true;
|
||||
return true;
|
||||
|
||||
case "0":
|
||||
case "no":
|
||||
result = false;
|
||||
return true;
|
||||
|
||||
|
@ -127,6 +127,8 @@ namespace osu.Game.Screens.Select
|
||||
private Sample sampleChangeDifficulty = null!;
|
||||
private Sample sampleChangeBeatmap = null!;
|
||||
|
||||
private bool pendingFilterApplication;
|
||||
|
||||
private Container carouselContainer = null!;
|
||||
|
||||
protected BeatmapDetailArea BeatmapDetails { get; private set; } = null!;
|
||||
@ -328,7 +330,20 @@ namespace osu.Game.Screens.Select
|
||||
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
|
||||
}, c => carouselContainer.Child = c);
|
||||
|
||||
FilterControl.FilterChanged = Carousel.Filter;
|
||||
FilterControl.FilterChanged = criteria =>
|
||||
{
|
||||
// If a filter operation is applied when we're in a state that doesn't allow selection,
|
||||
// we might end up in an unexpected state. This is because currently carousel panels are in charge
|
||||
// of updating the global selection (which is very hard to deal with).
|
||||
//
|
||||
// For now let's just avoid filtering when selection isn't allowed locally.
|
||||
// This should be nuked from existence when we get around to fixing the complexity of song select <-> beatmap carousel.
|
||||
// The debounce part of BeatmapCarousel's filtering should probably also be removed and handled locally.
|
||||
if (Carousel.AllowSelection)
|
||||
Carousel.Filter(criteria);
|
||||
else
|
||||
pendingFilterApplication = true;
|
||||
};
|
||||
|
||||
if (ShowSongSelectFooter)
|
||||
{
|
||||
@ -701,6 +716,12 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
Carousel.AllowSelection = true;
|
||||
|
||||
if (pendingFilterApplication)
|
||||
{
|
||||
Carousel.Filter(FilterControl.CreateCriteria());
|
||||
pendingFilterApplication = false;
|
||||
}
|
||||
|
||||
BeatmapDetails.Refresh();
|
||||
|
||||
beginLooping();
|
||||
|
@ -1,203 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
|
||||
/// </summary>
|
||||
public abstract partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0 };
|
||||
|
||||
private const double fade_out_duration = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Duration in milliseconds for the counter roll-up animation for each element.
|
||||
/// </summary>
|
||||
private const double rolling_duration = 20;
|
||||
|
||||
protected readonly LegacySpriteText PopOutCountText;
|
||||
protected readonly LegacySpriteText DisplayedCountText;
|
||||
|
||||
private int previousValue;
|
||||
|
||||
private int displayedCount;
|
||||
|
||||
private bool isRolling;
|
||||
|
||||
private readonly Container counterContainer;
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
protected LegacyComboCounter()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
counterContainer = new Container
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Children = new[]
|
||||
{
|
||||
PopOutCountText = new LegacySpriteText(LegacyFont.Combo)
|
||||
{
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
},
|
||||
DisplayedCountText = new LegacySpriteText(LegacyFont.Combo)
|
||||
{
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Value shown at the current moment.
|
||||
/// </summary>
|
||||
public virtual int DisplayedCount
|
||||
{
|
||||
get => displayedCount;
|
||||
private set
|
||||
{
|
||||
if (displayedCount.Equals(value))
|
||||
return;
|
||||
|
||||
if (isRolling)
|
||||
onDisplayedCountRolling(value);
|
||||
else if (displayedCount + 1 == value)
|
||||
onDisplayedCountIncrement(value);
|
||||
else
|
||||
onDisplayedCountChange(value);
|
||||
|
||||
displayedCount = value;
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
Current.BindTo(scoreProcessor.Combo);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
DisplayedCountText.Text = FormatCount(Current.Value);
|
||||
PopOutCountText.Text = FormatCount(Current.Value);
|
||||
|
||||
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
|
||||
|
||||
counterContainer.Size = DisplayedCountText.Size;
|
||||
}
|
||||
|
||||
private void updateCount(bool rolling)
|
||||
{
|
||||
int prev = previousValue;
|
||||
previousValue = Current.Value;
|
||||
|
||||
if (!IsLoaded)
|
||||
return;
|
||||
|
||||
if (!rolling)
|
||||
{
|
||||
FinishTransforms(false, nameof(DisplayedCount));
|
||||
|
||||
isRolling = false;
|
||||
DisplayedCount = prev;
|
||||
|
||||
if (prev + 1 == Current.Value)
|
||||
OnCountIncrement();
|
||||
else
|
||||
OnCountChange();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnCountRolling();
|
||||
isRolling = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the counter should display the new value with transitions.
|
||||
/// </summary>
|
||||
protected virtual void OnCountIncrement()
|
||||
{
|
||||
if (DisplayedCount < Current.Value - 1)
|
||||
DisplayedCount++;
|
||||
|
||||
DisplayedCount++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the counter should roll to the new combo value (usually roll back to zero).
|
||||
/// </summary>
|
||||
protected virtual void OnCountRolling()
|
||||
{
|
||||
// Hides displayed count if was increasing from 0 to 1 but didn't finish
|
||||
if (DisplayedCount == 0 && Current.Value == 0)
|
||||
DisplayedCountText.FadeOut(fade_out_duration);
|
||||
|
||||
transformRoll(DisplayedCount, Current.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the counter should display the new combo value without any transitions.
|
||||
/// </summary>
|
||||
protected virtual void OnCountChange()
|
||||
{
|
||||
if (Current.Value == 0)
|
||||
DisplayedCountText.FadeOut();
|
||||
|
||||
DisplayedCount = Current.Value;
|
||||
}
|
||||
|
||||
private void onDisplayedCountRolling(int newValue)
|
||||
{
|
||||
if (newValue == 0)
|
||||
DisplayedCountText.FadeOut(fade_out_duration);
|
||||
|
||||
DisplayedCountText.Text = FormatCount(newValue);
|
||||
counterContainer.Size = DisplayedCountText.Size;
|
||||
}
|
||||
|
||||
private void onDisplayedCountChange(int newValue)
|
||||
{
|
||||
DisplayedCountText.FadeTo(newValue == 0 ? 0 : 1);
|
||||
DisplayedCountText.Text = FormatCount(newValue);
|
||||
|
||||
counterContainer.Size = DisplayedCountText.Size;
|
||||
}
|
||||
|
||||
private void onDisplayedCountIncrement(int newValue)
|
||||
{
|
||||
DisplayedCountText.Text = FormatCount(newValue);
|
||||
|
||||
counterContainer.Size = DisplayedCountText.Size;
|
||||
}
|
||||
|
||||
private void transformRoll(int currentValue, int newValue) =>
|
||||
this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue));
|
||||
|
||||
protected virtual string FormatCount(int count) => $@"{count}";
|
||||
|
||||
private double getProportionalDuration(int currentValue, int newValue)
|
||||
{
|
||||
double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
|
||||
return difference * rolling_duration;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,12 @@
|
||||
// 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.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
@ -10,73 +14,265 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
|
||||
/// </summary>
|
||||
public partial class LegacyDefaultComboCounter : LegacyComboCounter
|
||||
public partial class LegacyDefaultComboCounter : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0 };
|
||||
|
||||
private uint scheduledPopOutCurrentId;
|
||||
|
||||
private const double big_pop_out_duration = 300;
|
||||
|
||||
private const double small_pop_out_duration = 100;
|
||||
|
||||
private ScheduledDelegate? scheduledPopOut;
|
||||
private const double fade_out_duration = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Duration in milliseconds for the counter roll-up animation for each element.
|
||||
/// </summary>
|
||||
private const double rolling_duration = 20;
|
||||
|
||||
private readonly Drawable popOutCount;
|
||||
|
||||
private readonly Drawable displayedCountSpriteText;
|
||||
|
||||
private int previousValue;
|
||||
|
||||
private int displayedCount;
|
||||
|
||||
private bool isRolling;
|
||||
|
||||
private readonly Container counterContainer;
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public LegacyDefaultComboCounter()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Anchor = Anchor.BottomLeft;
|
||||
Origin = Anchor.BottomLeft;
|
||||
|
||||
Margin = new MarginPadding(10);
|
||||
|
||||
PopOutCountText.Anchor = Anchor.BottomLeft;
|
||||
DisplayedCountText.Anchor = Anchor.BottomLeft;
|
||||
Scale = new Vector2(1.28f);
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
counterContainer = new Container
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Children = new[]
|
||||
{
|
||||
popOutCount = new LegacySpriteText(LegacyFont.Combo)
|
||||
{
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
},
|
||||
displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo)
|
||||
{
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Value shown at the current moment.
|
||||
/// </summary>
|
||||
public virtual int DisplayedCount
|
||||
{
|
||||
get => displayedCount;
|
||||
private set
|
||||
{
|
||||
if (displayedCount.Equals(value))
|
||||
return;
|
||||
|
||||
if (isRolling)
|
||||
onDisplayedCountRolling(value);
|
||||
else if (displayedCount + 1 == value)
|
||||
onDisplayedCountIncrement(value);
|
||||
else
|
||||
onDisplayedCountChange(value);
|
||||
|
||||
displayedCount = value;
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
Current.BindTo(scoreProcessor.Combo);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value);
|
||||
((IHasText)popOutCount).Text = formatCount(Current.Value);
|
||||
|
||||
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
|
||||
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
private void updateLayout()
|
||||
{
|
||||
const float font_height_ratio = 0.625f;
|
||||
const float vertical_offset = 9;
|
||||
|
||||
DisplayedCountText.OriginPosition = new Vector2(0, font_height_ratio * DisplayedCountText.Height + vertical_offset);
|
||||
DisplayedCountText.Position = new Vector2(0, -(1 - font_height_ratio) * DisplayedCountText.Height + vertical_offset);
|
||||
displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset);
|
||||
displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset);
|
||||
|
||||
PopOutCountText.OriginPosition = new Vector2(3, font_height_ratio * PopOutCountText.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left
|
||||
PopOutCountText.Position = new Vector2(0, -(1 - font_height_ratio) * PopOutCountText.Height + vertical_offset);
|
||||
popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left
|
||||
popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset);
|
||||
|
||||
counterContainer.Size = displayedCountSpriteText.Size;
|
||||
}
|
||||
|
||||
protected override void OnCountIncrement()
|
||||
private void updateCount(bool rolling)
|
||||
{
|
||||
DisplayedCountText.Show();
|
||||
int prev = previousValue;
|
||||
previousValue = Current.Value;
|
||||
|
||||
PopOutCountText.Text = FormatCount(Current.Value);
|
||||
if (!IsLoaded)
|
||||
return;
|
||||
|
||||
PopOutCountText.ScaleTo(1.56f)
|
||||
.ScaleTo(1, big_pop_out_duration);
|
||||
|
||||
PopOutCountText.FadeTo(0.6f)
|
||||
.FadeOut(big_pop_out_duration);
|
||||
|
||||
this.Delay(big_pop_out_duration - 140).Schedule(() =>
|
||||
if (!rolling)
|
||||
{
|
||||
base.OnCountIncrement();
|
||||
FinishTransforms(false, nameof(DisplayedCount));
|
||||
isRolling = false;
|
||||
DisplayedCount = prev;
|
||||
|
||||
DisplayedCountText.ScaleTo(1).Then()
|
||||
.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then()
|
||||
.ScaleTo(1, small_pop_out_duration / 2, Easing.Out);
|
||||
}, out scheduledPopOut);
|
||||
if (prev + 1 == Current.Value)
|
||||
onCountIncrement(prev, Current.Value);
|
||||
else
|
||||
onCountChange(Current.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
onCountRolling(displayedCount, Current.Value);
|
||||
isRolling = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCountRolling()
|
||||
private void transformPopOut(int newValue)
|
||||
{
|
||||
scheduledPopOut?.Cancel();
|
||||
scheduledPopOut = null;
|
||||
((IHasText)popOutCount).Text = formatCount(newValue);
|
||||
|
||||
base.OnCountRolling();
|
||||
popOutCount.ScaleTo(1.56f)
|
||||
.ScaleTo(1, big_pop_out_duration);
|
||||
|
||||
popOutCount.FadeTo(0.6f)
|
||||
.FadeOut(big_pop_out_duration);
|
||||
}
|
||||
|
||||
protected override void OnCountChange()
|
||||
private void transformNoPopOut(int newValue)
|
||||
{
|
||||
scheduledPopOut?.Cancel();
|
||||
scheduledPopOut = null;
|
||||
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
|
||||
|
||||
base.OnCountChange();
|
||||
counterContainer.Size = displayedCountSpriteText.Size;
|
||||
|
||||
displayedCountSpriteText.ScaleTo(1);
|
||||
}
|
||||
|
||||
protected override string FormatCount(int count) => $@"{count}x";
|
||||
private void transformPopOutSmall(int newValue)
|
||||
{
|
||||
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
|
||||
|
||||
counterContainer.Size = displayedCountSpriteText.Size;
|
||||
|
||||
displayedCountSpriteText.ScaleTo(1).Then()
|
||||
.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then()
|
||||
.ScaleTo(1, small_pop_out_duration / 2, Easing.Out);
|
||||
}
|
||||
|
||||
private void scheduledPopOutSmall(uint id)
|
||||
{
|
||||
// Too late; scheduled task invalidated
|
||||
if (id != scheduledPopOutCurrentId)
|
||||
return;
|
||||
|
||||
DisplayedCount++;
|
||||
}
|
||||
|
||||
private void onCountIncrement(int currentValue, int newValue)
|
||||
{
|
||||
scheduledPopOutCurrentId++;
|
||||
|
||||
if (DisplayedCount < currentValue)
|
||||
DisplayedCount++;
|
||||
|
||||
displayedCountSpriteText.Show();
|
||||
|
||||
transformPopOut(newValue);
|
||||
|
||||
uint newTaskId = scheduledPopOutCurrentId;
|
||||
|
||||
Scheduler.AddDelayed(delegate
|
||||
{
|
||||
scheduledPopOutSmall(newTaskId);
|
||||
}, big_pop_out_duration - 140);
|
||||
}
|
||||
|
||||
private void onCountRolling(int currentValue, int newValue)
|
||||
{
|
||||
scheduledPopOutCurrentId++;
|
||||
|
||||
// Hides displayed count if was increasing from 0 to 1 but didn't finish
|
||||
if (currentValue == 0 && newValue == 0)
|
||||
displayedCountSpriteText.FadeOut(fade_out_duration);
|
||||
|
||||
transformRoll(currentValue, newValue);
|
||||
}
|
||||
|
||||
private void onCountChange(int newValue)
|
||||
{
|
||||
scheduledPopOutCurrentId++;
|
||||
|
||||
if (newValue == 0)
|
||||
displayedCountSpriteText.FadeOut();
|
||||
|
||||
DisplayedCount = newValue;
|
||||
}
|
||||
|
||||
private void onDisplayedCountRolling(int newValue)
|
||||
{
|
||||
if (newValue == 0)
|
||||
displayedCountSpriteText.FadeOut(fade_out_duration);
|
||||
else
|
||||
displayedCountSpriteText.Show();
|
||||
|
||||
transformNoPopOut(newValue);
|
||||
}
|
||||
|
||||
private void onDisplayedCountChange(int newValue)
|
||||
{
|
||||
displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1);
|
||||
transformNoPopOut(newValue);
|
||||
}
|
||||
|
||||
private void onDisplayedCountIncrement(int newValue)
|
||||
{
|
||||
displayedCountSpriteText.Show();
|
||||
transformPopOutSmall(newValue);
|
||||
}
|
||||
|
||||
private void transformRoll(int currentValue, int newValue) =>
|
||||
this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue));
|
||||
|
||||
private string formatCount(int count) => $@"{count}x";
|
||||
|
||||
private double getProportionalDuration(int currentValue, int newValue)
|
||||
{
|
||||
double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
|
||||
return difference * rolling_duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,9 @@ namespace osu.Game.Utils
|
||||
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
|
||||
/// will return the flipped position in screen space coordinates.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction to flip towards.</param>
|
||||
/// <param name="quad">The quad surrounding all selected objects. The center of this determines the position of the axis.</param>
|
||||
/// <param name="position">The position to flip.</param>
|
||||
public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
|
||||
{
|
||||
var centre = quad.Centre;
|
||||
@ -69,6 +72,20 @@ namespace osu.Game.Utils
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a flip axis vector, a surrounding quad for all selected objects, and a position,
|
||||
/// will return the flipped position in screen space coordinates.
|
||||
/// </summary>
|
||||
/// <param name="axis">The vector indicating the direction to flip towards. This is perpendicular to the mirroring axis.</param>
|
||||
/// <param name="quad">The quad surrounding all selected objects. The center of this determines the position of the axis.</param>
|
||||
/// <param name="position">The position to flip.</param>
|
||||
public static Vector2 GetFlippedPosition(Vector2 axis, Quad quad, Vector2 position)
|
||||
{
|
||||
var centre = quad.Centre;
|
||||
|
||||
return position - 2 * Vector2.Dot(position - centre, axis) * axis;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
|
||||
/// will return the scaled position in screen space coordinates.
|
||||
@ -93,9 +110,9 @@ namespace osu.Game.Utils
|
||||
/// Given a scale multiplier, an origin, and a position,
|
||||
/// will return the scaled position in screen space coordinates.
|
||||
/// </summary>
|
||||
public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position)
|
||||
public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position, float axisRotation = 0)
|
||||
{
|
||||
return origin + (position - origin) * scale;
|
||||
return origin + RotateVector(RotateVector(position - origin, axisRotation) * scale, -axisRotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -127,7 +144,67 @@ namespace osu.Game.Utils
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
||||
public static Quad GetSurroundingQuad(IEnumerable<IHasPosition> hitObjects) =>
|
||||
GetSurroundingQuad(hitObjects.SelectMany(h =>
|
||||
GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the points that make up the convex hull of the provided points.
|
||||
/// </summary>
|
||||
/// <param name="points">The points to calculate a convex hull.</param>
|
||||
public static List<Vector2> GetConvexHull(IEnumerable<Vector2> points)
|
||||
{
|
||||
var pointsList = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToList();
|
||||
|
||||
if (pointsList.Count < 3)
|
||||
return pointsList;
|
||||
|
||||
var convexHullLower = new List<Vector2>
|
||||
{
|
||||
pointsList[0],
|
||||
pointsList[1]
|
||||
};
|
||||
var convexHullUpper = new List<Vector2>
|
||||
{
|
||||
pointsList[^1],
|
||||
pointsList[^2]
|
||||
};
|
||||
|
||||
// Build the lower hull.
|
||||
for (int i = 2; i < pointsList.Count; i++)
|
||||
{
|
||||
Vector2 c = pointsList[i];
|
||||
while (convexHullLower.Count > 1 && isClockwise(convexHullLower[^2], convexHullLower[^1], c))
|
||||
convexHullLower.RemoveAt(convexHullLower.Count - 1);
|
||||
|
||||
convexHullLower.Add(c);
|
||||
}
|
||||
|
||||
// Build the upper hull.
|
||||
for (int i = pointsList.Count - 3; i >= 0; i--)
|
||||
{
|
||||
Vector2 c = pointsList[i];
|
||||
while (convexHullUpper.Count > 1 && isClockwise(convexHullUpper[^2], convexHullUpper[^1], c))
|
||||
convexHullUpper.RemoveAt(convexHullUpper.Count - 1);
|
||||
|
||||
convexHullUpper.Add(c);
|
||||
}
|
||||
|
||||
convexHullLower.RemoveAt(convexHullLower.Count - 1);
|
||||
convexHullUpper.RemoveAt(convexHullUpper.Count - 1);
|
||||
|
||||
convexHullLower.AddRange(convexHullUpper);
|
||||
|
||||
return convexHullLower;
|
||||
|
||||
float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X;
|
||||
|
||||
bool isClockwise(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) >= 0;
|
||||
}
|
||||
|
||||
public static List<Vector2> GetConvexHull(IEnumerable<IHasPosition> hitObjects) =>
|
||||
GetConvexHull(enumerateStartAndEndPositions(hitObjects));
|
||||
|
||||
private static IEnumerable<Vector2> enumerateStartAndEndPositions(IEnumerable<IHasPosition> hitObjects) =>
|
||||
hitObjects.SelectMany(h =>
|
||||
{
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
@ -140,6 +217,6 @@ namespace osu.Game.Utils
|
||||
}
|
||||
|
||||
return new[] { h.Position };
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.904.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.916.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.904.0" />
|
||||
<PackageReference Include="Sentry" Version="4.3.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.904.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.916.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user