mirror of
https://github.com/ppy/osu.git
synced 2024-12-16 21:02:55 +08:00
Merge branch 'master' into rhythm-fixes
This commit is contained in:
commit
6507e3e406
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -133,7 +133,7 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install maui-ios
|
||||
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.916.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1007.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Desktop.Updater
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
restartToApplyUpdate();
|
||||
Task.Run(restartToApplyUpdate);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@ -88,7 +88,11 @@ namespace osu.Desktop.Updater
|
||||
{
|
||||
notification = new UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = restartToApplyUpdate,
|
||||
CompletionClickAction = () =>
|
||||
{
|
||||
Task.Run(restartToApplyUpdate);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
Schedule(() => notificationOverlay.Post(notification));
|
||||
@ -127,13 +131,10 @@ namespace osu.Desktop.Updater
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool restartToApplyUpdate()
|
||||
private async Task restartToApplyUpdate()
|
||||
{
|
||||
// TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665).
|
||||
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
|
||||
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
|
||||
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
||||
Schedule(() => game.AttemptExit());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" />
|
||||
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// 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 BenchmarkDotNet.Attributes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkGeometryUtils : BenchmarkTest
|
||||
{
|
||||
[Params(100, 1000, 2000, 4000, 8000, 10000)]
|
||||
public int N;
|
||||
|
||||
private Vector2[] points = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
points = new Vector2[N];
|
||||
|
||||
for (int i = 0; i < points.Length; ++i)
|
||||
points[i] = new Vector2(RNG.Next(512), RNG.Next(384));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void MinimumEnclosingCircle() => GeometryUtils.MinimumEnclosingCircle(points);
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
{
|
||||
var result = base.SnapForBlueprint(blueprint);
|
||||
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
|
||||
protected override void AddHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
|
||||
|
||||
[Test]
|
||||
public void TestFruitPlacementPosition()
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
|
||||
private void addMoveAndClickSteps(double time, float position, bool end = false)
|
||||
{
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Localisation;
|
||||
@ -31,6 +32,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
@ -223,10 +225,28 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new DifficultySection(),
|
||||
new ColoursSection(),
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(SetupScreen.SPACING),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ResourcesSection
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
new ColoursSection
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
}
|
||||
},
|
||||
new DesignSection(),
|
||||
];
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
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)),
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
return attributes;
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class BananaShowerCompositionTool : HitObjectCompositionTool
|
||||
public class BananaShowerCompositionTool : CompositionTool
|
||||
{
|
||||
public BananaShowerCompositionTool()
|
||||
: base(nameof(BananaShower))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public partial class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
|
||||
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
where THitObject : CatchHitObject, new()
|
||||
{
|
||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
@ -172,7 +173,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||
{
|
||||
editablePath.AddVertex(rightMouseDownPosition);
|
||||
});
|
||||
})
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new FruitCompositionTool(),
|
||||
new JuiceStreamCompositionTool(),
|
||||
@ -114,6 +114,26 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
handleToggleViaKey(e);
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
handleToggleViaKey(e);
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
private void handleToggleViaKey(KeyboardEvent key)
|
||||
{
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class FruitCompositionTool : HitObjectCompositionTool
|
||||
public class FruitCompositionTool : CompositionTool
|
||||
{
|
||||
public FruitCompositionTool()
|
||||
: base(nameof(Fruit))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class JuiceStreamCompositionTool : HitObjectCompositionTool
|
||||
public class JuiceStreamCompositionTool : CompositionTool
|
||||
{
|
||||
public JuiceStreamCompositionTool()
|
||||
: base(nameof(JuiceStream))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
});
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
{
|
||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
var pos = column.ScreenSpacePositionAtTime(time);
|
||||
|
@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestKeyCountChange()
|
||||
{
|
||||
LabelledSliderBar<float> keyCount = null!;
|
||||
FormSliderBar<float> keyCount = null!;
|
||||
|
||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<LabelledSliderBar<float>>().First(), () => Is.Not.Null);
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<float>>().First(), () => Is.Not.Null);
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddStep("change key count to 8", () =>
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -92,5 +93,30 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
|
||||
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOffScreenObjectsRemainSelectedOnColumnChange()
|
||||
{
|
||||
AddStep("create objects", () =>
|
||||
{
|
||||
for (int i = 0; i < 20; ++i)
|
||||
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = 0 });
|
||||
});
|
||||
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("start drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First());
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("end drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last());
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
|
||||
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public abstract partial class ManiaPlacementBlueprint<T> : PlacementBlueprint
|
||||
public abstract partial class ManiaPlacementBlueprint<T> : HitObjectPlacementBlueprint
|
||||
where T : ManiaHitObject
|
||||
{
|
||||
protected new T HitObject => (T)base.HitObject;
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class HoldNoteCompositionTool : HitObjectCompositionTool
|
||||
public class HoldNoteCompositionTool : CompositionTool
|
||||
{
|
||||
public HoldNoteCompositionTool()
|
||||
: base("Hold")
|
||||
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid();
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new NoteCompositionTool(),
|
||||
new HoldNoteCompositionTool()
|
||||
|
@ -104,8 +104,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
int minColumn = int.MaxValue;
|
||||
int maxColumn = int.MinValue;
|
||||
|
||||
var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>().ToArray();
|
||||
|
||||
// find min/max in an initial pass before actually performing the movement.
|
||||
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
|
||||
foreach (var obj in selectedObjects)
|
||||
{
|
||||
if (obj.Column < minColumn)
|
||||
minColumn = obj.Column;
|
||||
@ -121,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
((ManiaHitObject)h).Column += columnDelta;
|
||||
maniaPlayfield.Add(h);
|
||||
});
|
||||
|
||||
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
|
||||
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
|
||||
// (check blame for detailed explanation).
|
||||
// thus, ensure that selection is preserved manually.
|
||||
EditorBeatmap.SelectedHitObjects.Clear();
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(selectedObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class NoteCompositionTool : HitObjectCompositionTool
|
||||
public class NoteCompositionTool : CompositionTool
|
||||
{
|
||||
public NoteCompositionTool()
|
||||
: base(nameof(Note))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||
private LabelledSwitchButton specialStyle { get; set; } = null!;
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||
private FormCheckBox specialStyle { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
@ -37,77 +37,81 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
keyCountSlider = new LabelledSliderBar<float>
|
||||
keyCountSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsCsMania,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = "The number of columns in the beatmap",
|
||||
Caption = BeatmapsetsStrings.ShowStatsCsMania,
|
||||
HintText = "The number of columns in the beatmap",
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
specialStyle = new LabelledSwitchButton
|
||||
specialStyle = new FormCheckBox
|
||||
{
|
||||
Label = "Use special (N+1) style",
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||
Caption = "Use special (N+1) style",
|
||||
HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||
},
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||
HintText = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
overallDifficultySlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
tickRateSlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -419,9 +419,12 @@ namespace osu.Game.Rulesets.Mania
|
||||
return new ManiaFilterCriteria();
|
||||
}
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new ManiaDifficultySection(),
|
||||
new ResourcesSection(),
|
||||
new DesignSection(),
|
||||
];
|
||||
|
||||
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
||||
|
@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||
|
||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14102693012101306d, 2, "nan-slider")]
|
||||
[TestCase(6.7154251995274938d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4430610657612626d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(8.9742952703071666d, 239, "diffcalc-test")]
|
||||
[TestCase(1.743180218215227d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55071082800473514d, 4, "very-fast-slider")]
|
||||
[TestCase(8.9808183779700208d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7483507893412422d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||
|
||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7154251995274938d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4430610657612626d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
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;
|
||||
private const double distance_multiplier = 0.94;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of tapping the current object, based on:
|
||||
@ -38,12 +39,12 @@ 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);
|
||||
|
||||
// speedBonus will be 1.0 for BPM < 200
|
||||
double speedBonus = 1.0;
|
||||
// speedBonus will be 0.0 for BPM < 200
|
||||
double speedBonus = 0.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);
|
||||
speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||
|
||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||
@ -51,11 +52,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
// 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);
|
||||
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||
|
||||
// Base difficulty with all bonuses
|
||||
double difficulty = speedBonus * distanceBonus * 1000 / strainTime;
|
||||
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||
|
||||
// Apply penalty if there's doubletappable doubles
|
||||
return difficulty * doubletapness;
|
||||
|
@ -81,7 +81,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
int maxCombo = beatmap.GetMaxCombo();
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = maxCombo,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
HitCircleCount = hitCirclesCount,
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
|
||||
private double accuracy;
|
||||
private int scoreMaxCombo;
|
||||
@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
|
@ -9,7 +9,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
{
|
||||
public partial class HitCirclePlacementBlueprint : PlacementBlueprint
|
||||
public partial class HitCirclePlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
public new HitCircle HitObject => (HitCircle)base.HitObject;
|
||||
|
||||
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (segment.Count == 0)
|
||||
return;
|
||||
|
||||
var first = segment[0];
|
||||
PathControlPoint first = segment[0];
|
||||
|
||||
if (first.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (selectedPieces.Length != 1)
|
||||
return false;
|
||||
|
||||
var selectedPiece = selectedPieces.Single();
|
||||
var selectedPoint = selectedPiece.ControlPoint;
|
||||
PathControlPointPiece<T> selectedPiece = selectedPieces.Single();
|
||||
PathControlPoint selectedPoint = selectedPiece.ControlPoint;
|
||||
|
||||
var validTypes = path_types;
|
||||
PathType?[] validTypes = path_types;
|
||||
|
||||
if (selectedPoint == controlPoints[0])
|
||||
validTypes = validTypes.Where(t => t != null).ToArray();
|
||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (Pieces.All(p => !p.IsSelected.Value))
|
||||
return false;
|
||||
|
||||
var type = path_types[e.Key - Key.Number1];
|
||||
PathType? type = path_types[e.Key - Key.Number1];
|
||||
|
||||
// The first control point can never be inherit type
|
||||
if (Pieces[0].IsSelected.Value && type == null)
|
||||
@ -353,9 +353,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
double originalDistance = hitObject.Path.Distance;
|
||||
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
{
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
List<PathControlPoint> pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
@ -375,6 +377,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
EnsureValidPathTypes();
|
||||
|
||||
if (hitObject.Path.Distance < originalDistance)
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
else
|
||||
hitObject.Path.ExpectedDistance.Value = originalDistance;
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
@ -405,14 +412,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public void DragInProgress(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
var oldPosition = hitObject.Position;
|
||||
Vector2 oldPosition = hitObject.Position;
|
||||
double oldStartTime = hitObject.StartTime;
|
||||
|
||||
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
|
||||
{
|
||||
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
||||
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||
|
||||
@ -421,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
var controlPoint = hitObject.Path.ControlPoints[i];
|
||||
PathControlPoint controlPoint = hitObject.Path.ControlPoints[i];
|
||||
// Since control points are relative to the position of the hit object, all points that are _not_ selected
|
||||
// need to be offset _back_ by the delta corresponding to the movement of the head point.
|
||||
// All other selected control points (if any) will move together with the head point
|
||||
@ -432,13 +439,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||
|
||||
for (int i = 0; i < controlPoints.Count; ++i)
|
||||
{
|
||||
var controlPoint = controlPoints[i];
|
||||
PathControlPoint controlPoint = controlPoints[i];
|
||||
if (selectedControlPoints.Contains(controlPoint))
|
||||
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
||||
}
|
||||
@ -488,8 +495,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
curveTypeItems = new List<MenuItem>();
|
||||
|
||||
foreach (PathType? type in path_types)
|
||||
for (int i = 0; i < path_types.Length; ++i)
|
||||
{
|
||||
PathType? type = path_types[i];
|
||||
|
||||
// special inherit case
|
||||
if (type == null)
|
||||
{
|
||||
@ -499,7 +508,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||
}
|
||||
|
||||
curveTypeItems.Add(createMenuItemForPathType(type));
|
||||
curveTypeItems.Add(createMenuItemForPathType(type, InputKey.Number1 + i));
|
||||
}
|
||||
|
||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||
@ -533,7 +542,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
return menuItems.ToArray();
|
||||
|
||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
|
||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type, InputKey? key = null)
|
||||
{
|
||||
Hotkey hotkey = default;
|
||||
|
||||
if (key != null)
|
||||
hotkey = new Hotkey(new KeyCombination(InputKey.Alt, key.Value));
|
||||
|
||||
return new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)) { Hotkey = hotkey };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public partial class SliderPlacementBlueprint : PlacementBlueprint
|
||||
public partial class SliderPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
@ -401,7 +401,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
||||
else
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
||||
bodyPiece.UpdateFrom(HitObject);
|
||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
@ -269,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
}
|
||||
|
||||
@ -593,8 +594,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
changeHandler?.BeginChange();
|
||||
addControlPoint(lastRightClickPosition);
|
||||
changeHandler?.EndChange();
|
||||
}),
|
||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
||||
})
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
},
|
||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream)
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F))
|
||||
},
|
||||
};
|
||||
|
||||
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
|
||||
|
@ -13,7 +13,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||
{
|
||||
public partial class SpinnerPlacementBlueprint : PlacementBlueprint
|
||||
public partial class SpinnerPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
public new Spinner HitObject => (Spinner)base.HitObject;
|
||||
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class HitCircleCompositionTool : HitObjectCompositionTool
|
||||
public class HitCircleCompositionTool : CompositionTool
|
||||
{
|
||||
public HitCircleCompositionTool()
|
||||
: base(nameof(HitCircle))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
@ -90,6 +91,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private ExpandableSlider<float> gridLinesRotationSlider = null!;
|
||||
private EditorRadioButtonCollection gridTypeButtons = null!;
|
||||
|
||||
private ExpandableButton useSelectedObjectPositionButton = null!;
|
||||
|
||||
public OsuGridToolboxGroup()
|
||||
: base("grid")
|
||||
{
|
||||
@ -112,6 +115,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Current = StartPositionY,
|
||||
KeyboardStep = 1,
|
||||
},
|
||||
useSelectedObjectPositionButton = new ExpandableButton
|
||||
{
|
||||
ExpandedLabelText = "Centre on selected object",
|
||||
Action = () =>
|
||||
{
|
||||
if (editorBeatmap.SelectedHitObjects.Count != 1)
|
||||
return;
|
||||
|
||||
StartPosition.Value = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position;
|
||||
updateEnabledStates();
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
spacingSlider = new ExpandableSlider<float>
|
||||
{
|
||||
Current = Spacing,
|
||||
@ -186,12 +202,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
||||
}, true);
|
||||
|
||||
expandingContainer?.Expanded.BindValueChanged(v =>
|
||||
{
|
||||
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||
}, true);
|
||||
|
||||
GridType.BindValueChanged(v =>
|
||||
{
|
||||
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
||||
@ -211,6 +221,22 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
editorBeatmap.BeatmapReprocessed += updateEnabledStates;
|
||||
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates());
|
||||
expandingContainer?.Expanded.BindValueChanged(v =>
|
||||
{
|
||||
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||
updateEnabledStates();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void updateEnabledStates()
|
||||
{
|
||||
useSelectedObjectPositionButton.Enabled.Value = expandingContainer?.Expanded.Value == true
|
||||
&& editorBeatmap.SelectedHitObjects.Count == 1
|
||||
&& StartPosition.Value != ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position;
|
||||
}
|
||||
|
||||
private void nextGridSize()
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new HitCircleCompositionTool(),
|
||||
new SliderCompositionTool(),
|
||||
@ -369,6 +369,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
gridSnapMomentary = shiftPressed;
|
||||
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||
}
|
||||
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
||||
|
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||
|
||||
@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInRotation = selectedMovableObjects.ToArray();
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
|
||||
DefaultOrigin = GeometryUtils.MinimumEnclosingCircle(objectsInRotation).Item1;
|
||||
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
||||
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (!OperationInProgress.Value)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
|
||||
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && DefaultOrigin != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
Vector2 actualOrigin = origin ?? DefaultOrigin.Value;
|
||||
|
||||
foreach (var ho in objectsInRotation)
|
||||
{
|
||||
@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalPathControlPointPositions = null;
|
||||
defaultOrigin = null;
|
||||
DefaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
|
@ -84,10 +84,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||
? 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);
|
||||
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
@ -105,9 +105,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||
{
|
||||
var originalInfo = objectsInScale[slider];
|
||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation);
|
||||
scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -159,21 +157,25 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return scale;
|
||||
}
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0)
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2 origin, OriginalHitObjectState originalInfo, float axisRotation = 0)
|
||||
{
|
||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||
|
||||
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 = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation);
|
||||
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
||||
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
|
||||
slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
|
||||
}
|
||||
|
||||
// Snap the slider's length to the current beat divisor
|
||||
// to calculate the final resulting duration / bounding box before the final checks.
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
@ -182,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
|
||||
slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i];
|
||||
|
||||
slider.Position = originalInfo.Position;
|
||||
|
||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||
slider.SnapTo(snapProvider);
|
||||
|
@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osuTK;
|
||||
|
||||
@ -35,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private OsuCheckbox xCheckBox = null!;
|
||||
private OsuCheckbox yCheckBox = null!;
|
||||
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
|
||||
{
|
||||
this.scaleHandler = scaleHandler;
|
||||
@ -44,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
@ -191,14 +198,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
updateAxisCheckBoxesEnabled();
|
||||
}
|
||||
|
||||
private Vector2? getOriginPosition(PreciseScaleInfo scale) =>
|
||||
scale.Origin switch
|
||||
private Vector2? getOriginPosition(PreciseScaleInfo scale)
|
||||
{
|
||||
switch (scale.Origin)
|
||||
{
|
||||
ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||
ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||
ScaleOrigin.SelectionCentre => null,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(scale))
|
||||
};
|
||||
case ScaleOrigin.GridCentre:
|
||||
return gridToolbox.StartPosition.Value;
|
||||
|
||||
case ScaleOrigin.PlayfieldCentre:
|
||||
return OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
case ScaleOrigin.SelectionCentre:
|
||||
if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
|
||||
return slider.Position;
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(scale));
|
||||
}
|
||||
}
|
||||
|
||||
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
|
||||
|
||||
|
@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
public partial class OsuDifficultySection : SetupSection
|
||||
{
|
||||
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> stackLeniency { get; set; } = null!;
|
||||
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> stackLeniency { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
@ -31,103 +31,110 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
circleSizeSlider = new LabelledSliderBar<float>
|
||||
circleSizeSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsCs,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.CircleSizeDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsCs,
|
||||
HintText = EditorSetupStrings.CircleSizeDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||
HintText = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
approachRateSlider = new LabelledSliderBar<float>
|
||||
approachRateSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAr,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.ApproachRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAr,
|
||||
HintText = EditorSetupStrings.ApproachRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
overallDifficultySlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
tickRateSlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
stackLeniency = new LabelledSliderBar<float>
|
||||
stackLeniency = new FormSliderBar<float>
|
||||
{
|
||||
Label = "Stack Leniency",
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||
Caption = "Stack Leniency",
|
||||
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
||||
{
|
||||
Default = 0.7f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
Precision = 0.1f
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
||||
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
||||
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class SliderCompositionTool : HitObjectCompositionTool
|
||||
public class SliderCompositionTool : CompositionTool
|
||||
{
|
||||
public SliderCompositionTool()
|
||||
: base(nameof(Slider))
|
||||
@ -26,6 +26,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class SpinnerCompositionTool : HitObjectCompositionTool
|
||||
public class SpinnerCompositionTool : CompositionTool
|
||||
{
|
||||
public SpinnerCompositionTool()
|
||||
: base(nameof(Spinner))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -204,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
SpanStartTime = e.SpanStartTime,
|
||||
StartTime = e.Time,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
PathProgress = e.PathProgress,
|
||||
StackHeight = StackHeight,
|
||||
});
|
||||
break;
|
||||
@ -236,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
PathProgress = e.PathProgress,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -248,14 +250,27 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
endPositionCache.Invalidate();
|
||||
|
||||
if (HeadCircle != null)
|
||||
HeadCircle.Position = Position;
|
||||
foreach (var nested in NestedHitObjects)
|
||||
{
|
||||
switch (nested)
|
||||
{
|
||||
case SliderHeadCircle headCircle:
|
||||
headCircle.Position = Position;
|
||||
break;
|
||||
|
||||
if (TailCircle != null)
|
||||
TailCircle.Position = EndPosition;
|
||||
case SliderTailCircle tailCircle:
|
||||
tailCircle.Position = EndPosition;
|
||||
break;
|
||||
|
||||
if (LastRepeat != null)
|
||||
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
|
||||
case SliderRepeat repeat:
|
||||
repeat.Position = Position + Path.PositionAt(repeat.PathProgress);
|
||||
break;
|
||||
|
||||
case SliderTick tick:
|
||||
tick.Position = Position + Path.PositionAt(tick.PathProgress);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateNestedSamples()
|
||||
|
@ -5,6 +5,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SliderRepeat : SliderEndCircle
|
||||
{
|
||||
public double PathProgress { get; set; }
|
||||
|
||||
public SliderRepeat(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public int SpanIndex { get; set; }
|
||||
public double SpanStartTime { get; set; }
|
||||
public double PathProgress { get; set; }
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Localisation;
|
||||
@ -39,6 +40,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
@ -336,10 +338,28 @@ namespace osu.Game.Rulesets.Osu
|
||||
};
|
||||
}
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new OsuDifficultySection(),
|
||||
new ColoursSection(),
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(SetupScreen.SPACING),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ResourcesSection
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
new ColoursSection
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
}
|
||||
},
|
||||
new DesignSection(),
|
||||
];
|
||||
|
||||
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
||||
|
@ -232,8 +232,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
||||
workingObject.EndPositionModified = slider.EndPosition;
|
||||
|
||||
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
|
||||
|
||||
return workingObject.PositionModified - previousPosition;
|
||||
}
|
||||
|
||||
@ -307,22 +305,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
return new RectangleF(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
|
||||
/// </summary>
|
||||
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
|
||||
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
|
||||
private static void shiftNestedObjects(Slider slider, Vector2 shift)
|
||||
{
|
||||
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
|
||||
{
|
||||
if (!(hitObject is OsuHitObject osuHitObject))
|
||||
continue;
|
||||
|
||||
osuHitObject.Position += shift;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp a position to playfield, keeping a specified distance from the edges.
|
||||
/// </summary>
|
||||
@ -431,7 +413,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
private class WorkingObject
|
||||
{
|
||||
public float RotationOriginal { get; }
|
||||
public Vector2 PositionOriginal { get; }
|
||||
public Vector2 PositionModified { get; set; }
|
||||
public Vector2 EndPositionModified { get; set; }
|
||||
|
||||
@ -442,7 +423,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
{
|
||||
PositionInfo = positionInfo;
|
||||
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
|
||||
PositionModified = PositionOriginal = HitObject.Position;
|
||||
PositionModified = HitObject.Position;
|
||||
EndPositionModified = HitObject.EndPosition;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Argon;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class VolumeAwareHitSampleInfoTest
|
||||
{
|
||||
[Test]
|
||||
public void TestVolumeAwareHitSampleInfoIsNotEqualToItsUnderlyingSample(
|
||||
[Values(HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP)]
|
||||
string sample,
|
||||
[Values(HitSampleInfo.BANK_NORMAL, HitSampleInfo.BANK_SOFT)]
|
||||
string bank,
|
||||
[Values(30, 70, 100)] int volume)
|
||||
{
|
||||
var underlyingSample = new HitSampleInfo(sample, bank, volume: volume);
|
||||
var volumeAwareSample = new VolumeAwareHitSampleInfo(underlyingSample);
|
||||
|
||||
Assert.That(underlyingSample, Is.Not.EqualTo(volumeAwareSample));
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
ColourDifficulty = colourRating,
|
||||
PeakDifficulty = combinedRating,
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
return attributes;
|
||||
|
@ -10,7 +10,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
{
|
||||
public partial class HitPlacementBlueprint : PlacementBlueprint
|
||||
public partial class HitPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
private readonly HitPiece piece;
|
||||
|
||||
|
@ -17,7 +17,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
{
|
||||
public partial class TaikoSpanPlacementBlueprint : PlacementBlueprint
|
||||
public partial class TaikoSpanPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
private readonly HitPiece headPiece;
|
||||
private readonly HitPiece tailPiece;
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class DrumRollCompositionTool : HitObjectCompositionTool
|
||||
public class DrumRollCompositionTool : CompositionTool
|
||||
{
|
||||
public DrumRollCompositionTool()
|
||||
: base(nameof(DrumRoll))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class HitCompositionTool : HitObjectCompositionTool
|
||||
public class HitCompositionTool : CompositionTool
|
||||
{
|
||||
public HitCompositionTool()
|
||||
: base(nameof(Hit))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
||||
{
|
||||
public partial class TaikoDifficultySection : SetupSection
|
||||
{
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
@ -28,64 +28,68 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||
HintText = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
overallDifficultySlider = new FormSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
tickRateSlider = new FormSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
||||
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
||||
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class SwellCompositionTool : HitObjectCompositionTool
|
||||
public class SwellCompositionTool : CompositionTool
|
||||
{
|
||||
public SwellCompositionTool()
|
||||
: base(nameof(Swell))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new HitCompositionTool(),
|
||||
new DrumRollCompositionTool(),
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -86,10 +87,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
{
|
||||
if (selection.All(s => s.Item is Hit))
|
||||
yield return new TernaryStateToggleMenuItem("Rim") { State = { BindTarget = selectionRimState } };
|
||||
{
|
||||
yield return new TernaryStateToggleMenuItem("Rim")
|
||||
{
|
||||
State = { BindTarget = selectionRimState },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.W), new KeyCombination(InputKey.R)),
|
||||
};
|
||||
}
|
||||
|
||||
if (selection.All(s => s.Item is TaikoHitObject))
|
||||
yield return new TernaryStateToggleMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
||||
{
|
||||
yield return new TernaryStateToggleMenuItem("Strong")
|
||||
{
|
||||
State = { BindTarget = selectionStrongState },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.E)),
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
@ -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.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
@ -48,5 +49,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
return originalBank;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(HitSampleInfo? other) => other is VolumeAwareHitSampleInfo && base.Equals(other);
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This override attempts to match the <see cref="Equals"/> override above, but in theory it is not strictly necessary.
|
||||
/// Recall that <see cref="GetHashCode"/> <a href="https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-8.0#notes-to-inheritors">must meet the following requirements</a>:
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// "If two objects compare as equal, the <see cref="GetHashCode"/> method for each object must return the same value.
|
||||
/// However, if two objects do not compare as equal, <see cref="GetHashCode"/> methods for the two objects do not have to return different values."
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Making this override combine the value generated by the base <see cref="GetHashCode"/> implementation with a constant means
|
||||
/// that <see cref="HitSampleInfo"/> and <see cref="VolumeAwareHitSampleInfo"/> instances which have the same values of their members
|
||||
/// will not have equal hash codes, which is slightly more efficient when these objects are used as dictionary keys.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), 1);
|
||||
}
|
||||
}
|
||||
|
@ -190,9 +190,12 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new TaikoDifficultySection(),
|
||||
new ResourcesSection(),
|
||||
new DesignSection(),
|
||||
];
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
|
||||
|
@ -112,6 +112,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
SliderVelocityMultiplier = slider_velocity
|
||||
};
|
||||
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject));
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
|
||||
@ -227,26 +228,65 @@ namespace osu.Game.Tests.Editing
|
||||
assertSnappedDistance(400, 400);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnsnappedObject()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
// simulate object snapped to 1/3rds
|
||||
// this object's end time will be 2000 / 3 = 666.66... ms
|
||||
new PathControlPoint(new Vector2(200 / 3f, 0)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add slider", () => composer.EditorBeatmap.Add(slider));
|
||||
AddStep("set snap to 1/4", () => BeatDivisor.Value = 4);
|
||||
|
||||
// with default beat length of 1000ms and snap at 1/4, the valid snap times are 500ms, 750ms, and 1000ms
|
||||
// with default settings, the snapped distance will be a tenth of the difference of the time delta
|
||||
|
||||
// (500 - 666.66...) / 10 = -16.66... = -100 / 6
|
||||
assertSnappedDistance(0, -100 / 6f, slider);
|
||||
assertSnappedDistance(7, -100 / 6f, slider);
|
||||
|
||||
// (750 - 666.66...) / 10 = 8.33... = 100 / 12
|
||||
assertSnappedDistance(9, 100 / 12f, slider);
|
||||
assertSnappedDistance(33, 100 / 12f, slider);
|
||||
|
||||
// (1000 - 666.66...) / 10 = 33.33... = 100 / 3
|
||||
assertSnappedDistance(34, 100 / 3f, slider);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseCurrentSnap()
|
||||
{
|
||||
ExpandableButton getCurrentSnapButton() => composer.ChildrenOfType<EditorToolboxGroup>().Single(g => g.Name == "snapping")
|
||||
.ChildrenOfType<ExpandableButton>().Single();
|
||||
|
||||
AddStep("add objects to beatmap", () =>
|
||||
{
|
||||
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
||||
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
||||
});
|
||||
|
||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
|
||||
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
|
||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(getCurrentSnapButton()));
|
||||
AddUntilStep("use current snap expanded", () => getCurrentSnapButton().Expanded.Value, () => Is.True);
|
||||
|
||||
AddStep("seek before first object", () => EditorClock.Seek(0));
|
||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
||||
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("seek to between objects", () => EditorClock.Seek(1500));
|
||||
AddUntilStep("use current snap available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.True);
|
||||
AddUntilStep("use current snap available", () => getCurrentSnapButton().Enabled.Value, () => Is.True);
|
||||
|
||||
AddStep("seek after last object", () => EditorClock.Seek(2500));
|
||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
||||
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||
}
|
||||
|
||||
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
||||
@ -262,7 +302,7 @@ namespace osu.Game.Tests.Editing
|
||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
|
||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private partial class TestHitObjectComposer : OsuHitObjectComposer
|
||||
{
|
||||
|
52
osu.Game.Tests/Utils/BindableValueAccessorTest.cs
Normal file
52
osu.Game.Tests/Utils/BindableValueAccessorTest.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Utils
|
||||
{
|
||||
[TestFixture]
|
||||
public class BindableValueAccessorTest
|
||||
{
|
||||
[Test]
|
||||
public void GetValue()
|
||||
{
|
||||
const int value = 1337;
|
||||
|
||||
BindableInt bindable = new BindableInt(value);
|
||||
Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetValue()
|
||||
{
|
||||
const int value = 1337;
|
||||
|
||||
BindableInt bindable = new BindableInt();
|
||||
BindableValueAccessor.SetValue(bindable, value);
|
||||
|
||||
Assert.That(bindable.Value, Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetInvalidBindable()
|
||||
{
|
||||
BindableList<object> list = new BindableList<object>();
|
||||
Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetInvalidBindable()
|
||||
{
|
||||
const int value = 1337;
|
||||
|
||||
BindableList<int> list = new BindableList<int> { value };
|
||||
BindableValueAccessor.SetValue(list, 2);
|
||||
|
||||
Assert.That(list, Has.Exactly(1).Items);
|
||||
Assert.That(list[0], Is.EqualTo(value));
|
||||
}
|
||||
}
|
||||
}
|
@ -29,5 +29,23 @@ namespace osu.Game.Tests.Utils
|
||||
|
||||
Assert.That(hull, Is.EquivalentTo(expectedPoints));
|
||||
}
|
||||
|
||||
[TestCase(new int[] { }, 0, 0, 0)]
|
||||
[TestCase(new[] { 0, 0 }, 0, 0, 0)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, 1, 0, 1)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, 1, 0, 1)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, 3, 4.5f, 5.5901699f)]
|
||||
public void TestMinimumEnclosingCircle(int[] values, float x, float y, float r)
|
||||
{
|
||||
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 centre, float radius) = GeometryUtils.MinimumEnclosingCircle(points);
|
||||
|
||||
Assert.That(centre.X, Is.EqualTo(x).Within(0.0001));
|
||||
Assert.That(centre.Y, Is.EqualTo(y).Within(0.0001));
|
||||
Assert.That(radius, Is.EqualTo(r).Within(0.0001));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal file
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal file
@ -0,0 +1,123 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneColoursSection : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestNoBeatmapSkinColours()
|
||||
{
|
||||
LegacyBeatmapSkin skin = null!;
|
||||
ColoursSection coloursSection = null!;
|
||||
|
||||
AddStep("create beatmap skin", () => skin = new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||
AddStep("create colours section", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
|
||||
}, skin)),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
|
||||
],
|
||||
Child = coloursSection = new ColoursSection
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
});
|
||||
AddAssert("beatmap skin has no colours", () => skin.Configuration.CustomComboColours, () => Is.Empty);
|
||||
AddAssert("section displays default combo colours",
|
||||
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
|
||||
() => Is.EquivalentTo(new Colour4[]
|
||||
{
|
||||
SkinConfiguration.DefaultComboColours[1],
|
||||
SkinConfiguration.DefaultComboColours[2],
|
||||
SkinConfiguration.DefaultComboColours[3],
|
||||
SkinConfiguration.DefaultComboColours[0],
|
||||
}));
|
||||
|
||||
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
|
||||
AddAssert("beatmap skin has colours",
|
||||
() => skin.Configuration.CustomComboColours,
|
||||
() => Is.EquivalentTo(new[]
|
||||
{
|
||||
SkinConfiguration.DefaultComboColours[1],
|
||||
SkinConfiguration.DefaultComboColours[2],
|
||||
SkinConfiguration.DefaultComboColours[3],
|
||||
Color4.Aqua,
|
||||
SkinConfiguration.DefaultComboColours[0],
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExistingColours()
|
||||
{
|
||||
LegacyBeatmapSkin skin = null!;
|
||||
ColoursSection coloursSection = null!;
|
||||
|
||||
AddStep("create beatmap skin", () =>
|
||||
{
|
||||
skin = new LegacyBeatmapSkin(new BeatmapInfo(), null);
|
||||
skin.Configuration.CustomComboColours = new List<Color4>
|
||||
{
|
||||
Color4.Azure,
|
||||
Color4.Beige,
|
||||
Color4.Chartreuse
|
||||
};
|
||||
});
|
||||
AddStep("create colours section", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
|
||||
}, skin)),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
|
||||
],
|
||||
Child = coloursSection = new ColoursSection
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
});
|
||||
AddAssert("section displays combo colours",
|
||||
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
|
||||
() => Is.EquivalentTo(new[]
|
||||
{
|
||||
Colour4.Beige,
|
||||
Colour4.Chartreuse,
|
||||
Colour4.Azure,
|
||||
}));
|
||||
|
||||
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
|
||||
AddAssert("beatmap skin has colours",
|
||||
() => skin.Configuration.CustomComboColours,
|
||||
() => Is.EquivalentTo(new[]
|
||||
{
|
||||
Color4.Azure,
|
||||
Color4.Beige,
|
||||
Color4.Aqua,
|
||||
Color4.Chartreuse
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
@ -84,6 +84,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
targetContainer = getTargetContainer();
|
||||
initialRotation = targetContainer!.Rotation;
|
||||
DefaultOrigin = ToLocalSpace(targetContainer.ToScreenSpace(Vector2.Zero));
|
||||
|
||||
base.Begin();
|
||||
}
|
||||
|
@ -4,9 +4,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
@ -36,6 +38,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private ContextMenuContainer contextMenuContainer
|
||||
=> Editor.ChildrenOfType<ContextMenuContainer>().First();
|
||||
|
||||
private SelectionBoxScaleHandle getScaleHandle(Anchor anchor)
|
||||
=> Editor.ChildrenOfType<SelectionBoxScaleHandle>().First(it => it.Anchor == anchor);
|
||||
|
||||
private void moveMouseToObject(Func<HitObject> targetFunc)
|
||||
{
|
||||
AddStep("move mouse to object", () =>
|
||||
@ -78,7 +83,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNudgeSelection()
|
||||
public void TestNudgeSelectionTime()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
@ -99,6 +104,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNudgeSelectionPosition()
|
||||
{
|
||||
HitCircle addedObject = null!;
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
|
||||
{
|
||||
addedObject = new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||
}));
|
||||
|
||||
AddStep("select object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||
|
||||
AddStep("nudge up", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.Up);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("object position moved up", () => addedObject.Position.Y, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
AddStep("nudge down", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.Down);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("object position moved down", () => addedObject.Position.Y, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
AddStep("nudge left", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("object position moved left", () => addedObject.Position.X, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
AddStep("nudge right", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.Right);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("object position moved right", () => addedObject.Position.X, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRotateHotkeys()
|
||||
{
|
||||
@ -215,6 +265,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiSelectWithDragBox()
|
||||
{
|
||||
var addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100 },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||
new HitCircle { StartTime = 300, Position = new Vector2(512, 0) },
|
||||
new HitCircle { StartTime = 400, Position = new Vector2(412, 100) },
|
||||
};
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
|
||||
|
||||
AddStep("start dragging", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopLeft - new Vector2(5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||
|
||||
AddStep("start dragging with control", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
|
||||
|
||||
AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4));
|
||||
|
||||
AddStep("start dragging without control", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNearestSelection()
|
||||
{
|
||||
@ -519,5 +614,137 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShiftModifierMaintainsAspectRatio()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
float aspectRatioBeforeDrag = 0;
|
||||
|
||||
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
aspectRatioBeforeDrag = getAspectRatio();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAltModifierScalesAroundCenter()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||
|
||||
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
centerBeforeDrag = getCenter();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
|
||||
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShiftAndAltModifierKeys()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
float aspectRatioBeforeDrag = 0;
|
||||
|
||||
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||
|
||||
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||
|
||||
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
aspectRatioBeforeDrag = getAspectRatio();
|
||||
centerBeforeDrag = getCenter();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
|
||||
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,14 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private TestDesignSection designSection;
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUp()
|
||||
{
|
||||
@ -42,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
(typeof(EditorBeatmap), editorBeatmap)
|
||||
},
|
||||
Child = designSection = new TestDesignSection()
|
||||
Child = designSection = new TestDesignSection { RelativeSizeAxes = Axes.X }
|
||||
});
|
||||
}
|
||||
|
||||
@ -99,11 +104,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private partial class TestDesignSection : DesignSection
|
||||
{
|
||||
public new LabelledSwitchButton EnableCountdown => base.EnableCountdown;
|
||||
public new FormCheckBox EnableCountdown => base.EnableCountdown;
|
||||
|
||||
public new FillFlowContainer CountdownSettings => base.CountdownSettings;
|
||||
public new LabelledEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
||||
public new LabelledNumberBox CountdownOffset => base.CountdownOffset;
|
||||
public new FormEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
||||
public new FormTextBox CountdownOffset => base.CountdownOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK.Input;
|
||||
@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
|
||||
});
|
||||
|
||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
||||
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||
|
||||
if (i == 11)
|
||||
{
|
||||
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
EditorBeatmap.EndChange();
|
||||
});
|
||||
|
||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
||||
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||
|
||||
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
||||
|
@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
|
||||
|
||||
public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
|
||||
public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@ -135,9 +137,42 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
pressAndCheckTime(Key.Up, 0);
|
||||
}
|
||||
|
||||
private void pressAndCheckTime(Key key, double expectedTime)
|
||||
[Test]
|
||||
public void TestSeekBetweenObjects()
|
||||
{
|
||||
AddStep($"press {key}", () => InputManager.Key(key));
|
||||
AddStep("add objects", () =>
|
||||
{
|
||||
EditorBeatmap.Clear();
|
||||
EditorBeatmap.AddRange(new[]
|
||||
{
|
||||
new HitCircle { StartTime = 1000, },
|
||||
new HitCircle { StartTime = 2250, },
|
||||
new HitCircle { StartTime = 3600, },
|
||||
});
|
||||
});
|
||||
AddStep("seek to 0", () => EditorClock.Seek(0));
|
||||
|
||||
pressAndCheckTime(Key.Right, 1000, Key.ControlLeft);
|
||||
pressAndCheckTime(Key.Right, 2250, Key.ControlLeft);
|
||||
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
|
||||
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
|
||||
pressAndCheckTime(Key.Left, 2250, Key.ControlLeft);
|
||||
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
|
||||
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
|
||||
}
|
||||
|
||||
private void pressAndCheckTime(Key key, double expectedTime, params Key[] modifiers)
|
||||
{
|
||||
AddStep($"press {key} with {(modifiers.Any() ? string.Join(',', modifiers) : "no modifiers")}", () =>
|
||||
{
|
||||
foreach (var modifier in modifiers)
|
||||
InputManager.PressKey(modifier);
|
||||
|
||||
InputManager.Key(key);
|
||||
|
||||
foreach (var modifier in modifiers)
|
||||
InputManager.ReleaseKey(modifier);
|
||||
});
|
||||
AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,13 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
@ -20,6 +22,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[Cached]
|
||||
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
||||
{
|
||||
@ -201,7 +206,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
private void createSection()
|
||||
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
|
||||
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection { RelativeSizeAxes = Axes.X });
|
||||
|
||||
private void assertArtistMetadata(string expected)
|
||||
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
|
||||
@ -226,11 +231,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private partial class TestMetadataSection : MetadataSection
|
||||
{
|
||||
public new LabelledTextBox ArtistTextBox => base.ArtistTextBox;
|
||||
public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
||||
public new FormTextBox ArtistTextBox => base.ArtistTextBox;
|
||||
public new FormTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
||||
|
||||
public new LabelledTextBox TitleTextBox => base.TitleTextBox;
|
||||
public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
||||
public new FormTextBox TitleTextBox => base.TitleTextBox;
|
||||
public new FormTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,20 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
assertOnScreenAt(EditorScreenMode.Compose, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlDecodingOfArgs()
|
||||
{
|
||||
setUpEditor(new OsuRuleset().RulesetInfo);
|
||||
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("jump to encoded link", () => Game.HandleLink("osu://edit/00:14:142%20(1)"));
|
||||
|
||||
AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value);
|
||||
|
||||
AddAssert("time is correct", () => editorClock.CurrentTime, () => Is.EqualTo(14_142));
|
||||
AddAssert("selected object is correct", () => editorBeatmap.SelectedHitObjects.Single().StartTime, () => Is.EqualTo(14_142));
|
||||
}
|
||||
|
||||
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
|
||||
{
|
||||
AddStep($"{step} {timestamp}", () =>
|
||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
@ -167,14 +168,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestSpectatingDuringGameplay()
|
||||
{
|
||||
start();
|
||||
sendFrames(300);
|
||||
sendFrames(300, initialResultCount: 100);
|
||||
|
||||
loadSpectatingScreen();
|
||||
waitForPlayerCurrent();
|
||||
|
||||
sendFrames(300);
|
||||
sendFrames(300, initialResultCount: 100);
|
||||
|
||||
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000));
|
||||
AddAssert("check judgement counts are correct", () => player.ChildrenOfType<JudgementCountController>().Single().Counters.Sum(c => c.ResultCount.Value),
|
||||
() => Is.GreaterThanOrEqualTo(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private void checkPaused(bool state) =>
|
||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||
|
||||
private void sendFrames(int count = 10, double startTime = 0)
|
||||
private void sendFrames(int count = 10, double startTime = 0, int initialResultCount = 0)
|
||||
{
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime, initialResultCount));
|
||||
}
|
||||
|
||||
private void loadSpectatingScreen()
|
||||
|
@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||
|
||||
AddStep("disable shuffle", () => Game.MusicController.Shuffle.Value = false);
|
||||
|
||||
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||
|
||||
|
@ -5,14 +5,18 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
@ -23,6 +27,7 @@ using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Ranking.Statistics.User;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@ -80,6 +85,69 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
loadPanel(null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStatisticsShownCorrectlyIfUpdateDeliveredBeforeLoad()
|
||||
{
|
||||
UserStatisticsWatcher userStatisticsWatcher = null!;
|
||||
ScoreInfo score = null!;
|
||||
|
||||
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher()));
|
||||
AddStep("set user statistics update", () =>
|
||||
{
|
||||
score = TestResources.CreateTestScoreInfo();
|
||||
score.OnlineID = 1234;
|
||||
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score,
|
||||
new UserStatistics
|
||||
{
|
||||
Level = new UserStatistics.LevelInfo
|
||||
{
|
||||
Current = 5,
|
||||
Progress = 20,
|
||||
},
|
||||
GlobalRank = 38000,
|
||||
CountryRank = 12006,
|
||||
PP = 2134,
|
||||
RankedScore = 21123849,
|
||||
Accuracy = 0.985,
|
||||
PlayCount = 13375,
|
||||
PlayTime = 354490,
|
||||
TotalScore = 128749597,
|
||||
TotalHits = 0,
|
||||
MaxCombo = 1233,
|
||||
}, new UserStatistics
|
||||
{
|
||||
Level = new UserStatistics.LevelInfo
|
||||
{
|
||||
Current = 5,
|
||||
Progress = 30,
|
||||
},
|
||||
GlobalRank = 36000,
|
||||
CountryRank = 12000,
|
||||
PP = (decimal)2134.5,
|
||||
RankedScore = 23897015,
|
||||
Accuracy = 0.984,
|
||||
PlayCount = 13376,
|
||||
PlayTime = 35789,
|
||||
TotalScore = 132218497,
|
||||
TotalHits = 0,
|
||||
MaxCombo = 1233,
|
||||
});
|
||||
});
|
||||
AddStep("load user statistics panel", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)],
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new UserStatisticsPanel(score)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
Score = { Value = score, }
|
||||
}
|
||||
});
|
||||
AddUntilStep("overall ranking present", () => this.ChildrenOfType<OverallRanking>().Any());
|
||||
AddUntilStep("loading spinner not visible", () => this.ChildrenOfType<LoadingLayer>().All(l => l.State.Value == Visibility.Hidden));
|
||||
}
|
||||
|
||||
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
|
||||
{
|
||||
Child = new UserStatisticsPanel(score)
|
||||
|
@ -9,6 +9,11 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
public partial class TestSceneDirectorySelector : ThemeComparisonTestScene
|
||||
{
|
||||
public TestSceneDirectorySelector()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => new OsuDirectorySelector
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
|
@ -1,37 +1,49 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
public partial class TestSceneFileSelector : ThemeComparisonTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
public TestSceneFileSelector()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestJpgFilesOnly()
|
||||
{
|
||||
AddStep("create", () =>
|
||||
{
|
||||
ContentContainer.Children = new Drawable[]
|
||||
var colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
ContentContainer.Child = new DependencyProvidingContainer
|
||||
{
|
||||
new Box
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.GreySeaFoam
|
||||
(typeof(OverlayColourProvider), colourProvider)
|
||||
},
|
||||
new OsuFileSelector(validFileExtensions: new[] { ".jpg" })
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3
|
||||
},
|
||||
new OsuFileSelector(validFileExtensions: new[] { ".jpg" })
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@ -19,16 +20,20 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Width = 0.5f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(50),
|
||||
ChildrenEnumerable = new TestTargetClass().CreateSettingsControls()
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Width = 0.5f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(50),
|
||||
ChildrenEnumerable = new TestTargetClass().CreateSettingsControls()
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -66,6 +71,13 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
|
||||
[SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))]
|
||||
public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>();
|
||||
|
||||
[SettingSource("Sample colour", "Change the colour", SettingControlType = typeof(SettingsColour))]
|
||||
public BindableColour4 ColourBindable { get; } = new BindableColour4
|
||||
{
|
||||
Default = Colour4.White,
|
||||
Value = Colour4.Red
|
||||
};
|
||||
}
|
||||
|
||||
private enum TestEnum
|
||||
|
@ -175,6 +175,29 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
increaseModSpeed();
|
||||
AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed);
|
||||
|
||||
OsuModDoubleTime dtWithAdjustPitch = new OsuModDoubleTime
|
||||
{
|
||||
SpeedChange = { Value = 1.05 },
|
||||
AdjustPitch = { Value = true },
|
||||
};
|
||||
changeMods(dtWithAdjustPitch);
|
||||
|
||||
decreaseModSpeed();
|
||||
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
|
||||
|
||||
decreaseModSpeed();
|
||||
AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||
AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
|
||||
|
||||
AddStep("turn off adjust pitch", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value = false);
|
||||
|
||||
increaseModSpeed();
|
||||
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
|
||||
|
||||
increaseModSpeed();
|
||||
AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||
AddAssert("double time has adjust pitch inactive", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.False);
|
||||
|
||||
void increaseModSpeed() => AddStep("increase mod speed", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osuTK;
|
||||
@ -28,9 +29,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private TestCancellableCommentEditor cancellableCommentEditor = null!;
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
Add(new FillFlowContainer
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create content", () => Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -43,7 +45,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
commentEditor = new TestCommentEditor(),
|
||||
cancellableCommentEditor = new TestCancellableCommentEditor()
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommitViaKeyboard()
|
||||
@ -133,6 +136,34 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
assertLoggedInState();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommentsDisabled()
|
||||
{
|
||||
AddStep("no reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||
{
|
||||
CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes(),
|
||||
});
|
||||
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.False);
|
||||
|
||||
AddStep("specific reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||
{
|
||||
CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes
|
||||
{
|
||||
CanNewCommentReason = "This comment section is disabled. For reasons.",
|
||||
}
|
||||
});
|
||||
AddAssert("textbox disabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.True);
|
||||
|
||||
AddStep("entire commentable meta missing", () => commentEditor.CommentableMeta.Value = null);
|
||||
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.False);
|
||||
|
||||
AddStep("current user attributes missing", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||
{
|
||||
CurrentUserAttributes = null,
|
||||
});
|
||||
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCancelAction()
|
||||
{
|
||||
@ -167,8 +198,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"Commit" : "You're logged out!";
|
||||
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it.";
|
||||
protected override LocalisableString GetPlaceholderText() => @"This text box is empty";
|
||||
}
|
||||
|
||||
private partial class TestCancellableCommentEditor : CancellableCommentEditor
|
||||
@ -189,7 +219,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}
|
||||
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save";
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon";
|
||||
protected override LocalisableString GetPlaceholderText() => @"Multiline textboxes soon";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
@ -71,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Instantaneous slider",
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
@ -81,18 +82,26 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
new FormEnumDropdown<CountdownType>
|
||||
{
|
||||
Caption = "Non-instantaneous slider",
|
||||
Current = new BindableFloat
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "Audio file",
|
||||
PlaceholderText = "Select an audio file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
Caption = "Combo colours",
|
||||
Colours =
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
Instantaneous = false,
|
||||
TabbableContentContainer = this,
|
||||
Colour4.Red,
|
||||
Colour4.Green,
|
||||
Colour4.Blue,
|
||||
Colour4.Yellow,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -0,0 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneFormSliderBar : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[Test]
|
||||
public void TestTransferValueOnCommit()
|
||||
{
|
||||
OsuSpriteText text;
|
||||
FormSliderBar<float> slider = null!;
|
||||
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 0.5f,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
text = new OsuSpriteText(),
|
||||
slider = new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
Default = 5f,
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
slider.Current.BindValueChanged(_ => text.Text = $"Current value is: {slider.Current.Value}", true);
|
||||
});
|
||||
AddToggleStep("toggle transfer value on commit", b =>
|
||||
{
|
||||
if (slider.IsNotNull())
|
||||
slider.TransferValueOnCommit = b;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneHotkeyDisplay : ThemeComparisonTestScene
|
||||
{
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new[]
|
||||
{
|
||||
new HotkeyDisplay { Hotkey = new Hotkey(new KeyCombination(InputKey.MouseLeft)) },
|
||||
new HotkeyDisplay { Hotkey = new Hotkey(GlobalAction.EditorDecreaseDistanceSpacing) },
|
||||
new HotkeyDisplay { Hotkey = new Hotkey(PlatformAction.Save) },
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneSettingsColour : OsuManualInputManagerTestScene
|
||||
{
|
||||
private SettingsColour? component;
|
||||
|
||||
[Test]
|
||||
public void TestColour()
|
||||
{
|
||||
createContent();
|
||||
|
||||
AddRepeatStep("set random colour", () => component!.Current.Value = randomColour(), 4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserInteractions()
|
||||
{
|
||||
createContent();
|
||||
|
||||
AddStep("click colour", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(component!);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("colour picker spawned", () => this.ChildrenOfType<OsuColourPicker>().Any());
|
||||
}
|
||||
|
||||
private void createContent()
|
||||
{
|
||||
AddStep("create component", () =>
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 500,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
component = new SettingsColour
|
||||
{
|
||||
LabelText = "a sample component",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private Colour4 randomColour() => new Color4(
|
||||
RNG.NextSingle(),
|
||||
RNG.NextSingle(),
|
||||
RNG.NextSingle(),
|
||||
1);
|
||||
}
|
||||
}
|
@ -27,6 +27,9 @@ namespace osu.Game.Tournament.Screens.Setup
|
||||
[Resolved]
|
||||
private MatchIPCInfo ipc { get; set; } = null!;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private OsuDirectorySelector directorySelector = null!;
|
||||
private DialogOverlay? overlay;
|
||||
|
||||
|
@ -96,7 +96,7 @@ namespace osu.Game.Audio
|
||||
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
|
||||
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume));
|
||||
|
||||
public bool Equals(HitSampleInfo? other)
|
||||
public virtual bool Equals(HitSampleInfo? other)
|
||||
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
|
@ -198,8 +198,11 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
if (beatmapSet.OnlineID > 0)
|
||||
{
|
||||
// Required local for iOS. Will cause runtime crash if inlined.
|
||||
int onlineId = beatmapSet.OnlineID;
|
||||
|
||||
// OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure.
|
||||
foreach (var existingSetWithSameOnlineID in realm.All<BeatmapSetInfo>().Where(b => b.OnlineID == beatmapSet.OnlineID))
|
||||
foreach (var existingSetWithSameOnlineID in realm.All<BeatmapSetInfo>().Where(b => b.OnlineID == onlineId))
|
||||
{
|
||||
existingSetWithSameOnlineID.DeletePending = true;
|
||||
existingSetWithSameOnlineID.OnlineID = -1;
|
||||
|
@ -183,7 +183,14 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
#region Beatmap
|
||||
|
||||
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
|
||||
public virtual bool BeatmapLoaded
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (beatmapFetchLock)
|
||||
return beatmapLoadTask?.IsCompleted ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
public IBeatmap Beatmap
|
||||
{
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
@ -15,6 +14,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
@ -186,6 +186,16 @@ namespace osu.Game.Configuration
|
||||
|
||||
break;
|
||||
|
||||
case BindableColour4 bColour:
|
||||
yield return new SettingsColour
|
||||
{
|
||||
LabelText = attr.Label,
|
||||
TooltipText = attr.Description,
|
||||
Current = bColour
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case IBindable bindable:
|
||||
var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
|
||||
var dropdown = (Drawable)Activator.CreateInstance(dropdownType)!;
|
||||
@ -227,11 +237,11 @@ namespace osu.Game.Configuration
|
||||
case Bindable<bool> b:
|
||||
return b.Value;
|
||||
|
||||
case BindableColour4 c:
|
||||
return c.Value.ToHex();
|
||||
|
||||
case IBindable u:
|
||||
// An unknown (e.g. enum) generic type.
|
||||
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
|
||||
Debug.Assert(valueMethod != null);
|
||||
return valueMethod.GetValue(u)!;
|
||||
return BindableValueAccessor.GetValue(u);
|
||||
|
||||
default:
|
||||
// fall back for non-bindable cases.
|
||||
|
@ -3,8 +3,10 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -20,12 +22,15 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class DrawableOsuMenuItem : Menu.DrawableMenuItem
|
||||
{
|
||||
public const int MARGIN_HORIZONTAL = 17;
|
||||
public const int MARGIN_HORIZONTAL = 10;
|
||||
public const int MARGIN_VERTICAL = 4;
|
||||
private const int text_size = 17;
|
||||
private const int transition_length = 80;
|
||||
public const int TEXT_SIZE = 17;
|
||||
public const int TRANSITION_LENGTH = 80;
|
||||
|
||||
public BindableBool ShowCheckbox { get; } = new BindableBool();
|
||||
|
||||
private TextContainer text;
|
||||
private HotkeyDisplay hotkey;
|
||||
private HoverClickSounds hoverClickSounds;
|
||||
|
||||
public DrawableOsuMenuItem(MenuItem item)
|
||||
@ -39,43 +44,47 @@ namespace osu.Game.Graphics.UserInterface
|
||||
BackgroundColour = Color4.Transparent;
|
||||
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
|
||||
|
||||
AddInternal(hotkey = new HotkeyDisplay
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Margin = new MarginPadding { Right = 10, Top = 1 },
|
||||
});
|
||||
AddInternal(hoverClickSounds = new HoverClickSounds());
|
||||
|
||||
updateTextColour();
|
||||
updateText();
|
||||
|
||||
bool hasSubmenu = Item.Items.Any();
|
||||
|
||||
// Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`).
|
||||
if (hasSubmenu && RelativeSizeAxes == Axes.X)
|
||||
if (showChevron)
|
||||
{
|
||||
AddInternal(new SpriteIcon
|
||||
{
|
||||
Margin = new MarginPadding(6),
|
||||
Margin = new MarginPadding { Horizontal = 10, },
|
||||
Size = new Vector2(8),
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
});
|
||||
|
||||
text.Padding = new MarginPadding
|
||||
{
|
||||
// Add some padding for the chevron above.
|
||||
Right = 5,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`).
|
||||
private bool showChevron => Item.Items.Any() && RelativeSizeAxes == Axes.X;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ShowCheckbox.BindValueChanged(_ => updateState());
|
||||
Item.Action.BindDisabledChanged(_ => updateState(), true);
|
||||
FinishTransforms();
|
||||
}
|
||||
|
||||
private void updateTextColour()
|
||||
private void updateText()
|
||||
{
|
||||
switch ((Item as OsuMenuItem)?.Type)
|
||||
var osuMenuItem = Item as OsuMenuItem;
|
||||
|
||||
switch (osuMenuItem?.Type)
|
||||
{
|
||||
default:
|
||||
case MenuItemType.Standard:
|
||||
@ -90,6 +99,20 @@ namespace osu.Game.Graphics.UserInterface
|
||||
text.Colour = Color4Extensions.FromHex(@"ffcc22");
|
||||
break;
|
||||
}
|
||||
|
||||
hotkey.Hotkey = osuMenuItem?.Hotkey ?? default;
|
||||
hotkey.Alpha = EqualityComparer<Hotkey>.Default.Equals(hotkey.Hotkey, default) ? 0 : 1;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
// this hack ensures that the menu can auto-size while leaving enough space for the hotkey display.
|
||||
// the gist of it is that while the hotkey display is not in the text / "content" that determines sizing
|
||||
// (because it cannot be, because we want the hotkey display to align to the *right* and not the left),
|
||||
// enough padding to fit the hotkey with _its_ spacing is added as padding of the text to compensate.
|
||||
text.Padding = new MarginPadding { Right = hotkey.Alpha > 0 || showChevron ? hotkey.DrawWidth + 15 : 0 };
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@ -111,14 +134,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
if (IsHovered && IsActionable)
|
||||
{
|
||||
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
|
||||
text.BoldText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
text.NormalText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.BoldText.FadeOut(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeIn(transition_length, Easing.OutQuint);
|
||||
text.BoldText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
text.NormalText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
text.CheckboxContainer.Alpha = ShowCheckbox.Value ? 1 : 0;
|
||||
}
|
||||
|
||||
protected sealed override Drawable CreateContent() => text = CreateTextContainer();
|
||||
@ -138,32 +163,53 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public readonly SpriteText NormalText;
|
||||
public readonly SpriteText BoldText;
|
||||
public readonly Container CheckboxContainer;
|
||||
|
||||
public TextContainer()
|
||||
{
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
NormalText = new OsuSpriteText
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(10),
|
||||
Direction = FillDirection.Horizontal,
|
||||
Padding = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL, },
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: text_size),
|
||||
Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL },
|
||||
},
|
||||
BoldText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL },
|
||||
CheckboxContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = MARGIN_HORIZONTAL,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
NormalText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE),
|
||||
},
|
||||
BoldText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -46,12 +46,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
state = menuItem.State.GetBoundCopy();
|
||||
|
||||
Add(stateIcon = new SpriteIcon
|
||||
CheckboxContainer.Add(stateIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(10),
|
||||
Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL },
|
||||
AlwaysPresent = true,
|
||||
});
|
||||
}
|
||||
@ -62,14 +61,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
state.BindValueChanged(updateState, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Todo: This is bad. This can maybe be done better with a refactor of DrawableOsuMenuItem.
|
||||
stateIcon.X = BoldText.DrawWidth + 10;
|
||||
}
|
||||
|
||||
private void updateState(ValueChangedEvent<object> state)
|
||||
{
|
||||
var icon = menuItem.GetIconForState(state.NewValue);
|
||||
|
59
osu.Game/Graphics/UserInterface/Hotkey.cs
Normal file
59
osu.Game/Graphics/UserInterface/Hotkey.cs
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public readonly record struct Hotkey
|
||||
{
|
||||
public KeyCombination[]? KeyCombinations { get; init; }
|
||||
public GlobalAction? GlobalAction { get; init; }
|
||||
public PlatformAction? PlatformAction { get; init; }
|
||||
|
||||
public Hotkey(params KeyCombination[] keyCombinations)
|
||||
{
|
||||
KeyCombinations = keyCombinations;
|
||||
}
|
||||
|
||||
public Hotkey(GlobalAction globalAction)
|
||||
{
|
||||
GlobalAction = globalAction;
|
||||
}
|
||||
|
||||
public Hotkey(PlatformAction platformAction)
|
||||
{
|
||||
PlatformAction = platformAction;
|
||||
}
|
||||
|
||||
public IEnumerable<string> ResolveKeyCombination(ReadableKeyCombinationProvider keyCombinationProvider, RealmKeyBindingStore keyBindingStore, GameHost gameHost)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (KeyCombinations != null)
|
||||
{
|
||||
result.AddRange(KeyCombinations.Select(keyCombinationProvider.GetReadableString));
|
||||
}
|
||||
|
||||
if (GlobalAction != null)
|
||||
{
|
||||
result.AddRange(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.Value));
|
||||
}
|
||||
|
||||
if (PlatformAction != null)
|
||||
{
|
||||
var action = PlatformAction.Value;
|
||||
var bindings = gameHost.PlatformKeyBindings.Where(kb => (PlatformAction)kb.Action == action);
|
||||
result.AddRange(bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
110
osu.Game/Graphics/UserInterface/HotkeyDisplay.cs
Normal file
110
osu.Game/Graphics/UserInterface/HotkeyDisplay.cs
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class HotkeyDisplay : CompositeDrawable
|
||||
{
|
||||
private Hotkey hotkey;
|
||||
|
||||
public Hotkey Hotkey
|
||||
{
|
||||
get => hotkey;
|
||||
set
|
||||
{
|
||||
if (EqualityComparer<Hotkey>.Default.Equals(hotkey, value))
|
||||
return;
|
||||
|
||||
hotkey = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
private FillFlowContainer flow = null!;
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider readableKeyCombinationProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmKeyBindingStore realmKeyBindingStore { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost gameHost { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = flow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5)
|
||||
};
|
||||
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
flow.Clear();
|
||||
foreach (string h in hotkey.ResolveKeyCombination(readableKeyCombinationProvider, realmKeyBindingStore, gameHost))
|
||||
flow.Add(new HotkeyBox(h));
|
||||
}
|
||||
|
||||
private partial class HotkeyBox : CompositeDrawable
|
||||
{
|
||||
private readonly string hotkey;
|
||||
|
||||
public HotkeyBox(string hotkey)
|
||||
{
|
||||
this.hotkey = hotkey;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
CornerRadius = 3;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider?.Background6 ?? Colour4.Black.Opacity(0.7f),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding { Horizontal = 5, Bottom = 1, },
|
||||
Text = hotkey.ToUpperInvariant(),
|
||||
Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold),
|
||||
Colour = colourProvider?.Light1 ?? colours.GrayA,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user