mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 20:20:21 +08:00
Compare commits
654 Commits
@@ -114,10 +114,7 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET workloads
|
||||
# since windows image 20241113.3.0, not specifying a version here
|
||||
# installs the .NET 7 version of android workload for very unknown reasons.
|
||||
# revisit once we upgrade to .NET 9, it's probably fixed there.
|
||||
run: dotnet workload install android --version (dotnet --version)
|
||||
run: dotnet workload install android
|
||||
|
||||
- name: Compile
|
||||
run: dotnet build -c Debug osu.Android.slnf
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="osu.Android">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.115.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.225.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Android.Content.PM;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
public partial class GameplayScreenRotationLocker : Component
|
||||
{
|
||||
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameActivity gameActivity { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ILocalUserPlayInfo localUserPlayInfo)
|
||||
{
|
||||
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
|
||||
localUserPlaying.BindValueChanged(updateLock, true);
|
||||
}
|
||||
|
||||
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
|
||||
{
|
||||
gameActivity.RunOnUiThread(() =>
|
||||
{
|
||||
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,8 @@ namespace osu.Android
|
||||
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
||||
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
||||
|
||||
public new bool IsTablet { get; private set; }
|
||||
|
||||
private readonly OsuGameAndroid game;
|
||||
|
||||
private bool gameCreated;
|
||||
@@ -89,9 +91,9 @@ namespace osu.Android
|
||||
WindowManager.DefaultDisplay.GetSize(displaySize);
|
||||
#pragma warning restore CA1422
|
||||
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
|
||||
bool isTablet = smallestWidthDp >= 600f;
|
||||
IsTablet = smallestWidthDp >= 600f;
|
||||
|
||||
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
|
||||
RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
|
||||
|
||||
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
|
||||
// The assembly files are not available as files either after native AOT.
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
|
||||
using System;
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Microsoft.Maui.Devices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
@@ -18,6 +21,8 @@ namespace osu.Android
|
||||
[Cached]
|
||||
private readonly OsuGameActivity gameActivity;
|
||||
|
||||
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
||||
|
||||
public OsuGameAndroid(OsuGameActivity activity)
|
||||
: base(null)
|
||||
{
|
||||
@@ -71,7 +76,35 @@ namespace osu.Android
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
|
||||
UserPlayingState.BindValueChanged(_ => updateOrientation());
|
||||
}
|
||||
|
||||
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
|
||||
{
|
||||
base.ScreenChanged(current, newScreen);
|
||||
|
||||
if (newScreen != null)
|
||||
updateOrientation();
|
||||
}
|
||||
|
||||
private void updateOrientation()
|
||||
{
|
||||
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet);
|
||||
|
||||
switch (orientation)
|
||||
{
|
||||
case MobileUtils.Orientation.Locked:
|
||||
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
|
||||
break;
|
||||
|
||||
case MobileUtils.Orientation.Portrait:
|
||||
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
|
||||
break;
|
||||
|
||||
case MobileUtils.Orientation.Default:
|
||||
gameActivity.RequestedOrientation = gameActivity.DefaultOrientation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetHost(GameHost host)
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace osu.Desktop
|
||||
};
|
||||
|
||||
client.OnReady += onReady;
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -173,7 +173,7 @@ namespace osu.Desktop
|
||||
new Button
|
||||
{
|
||||
Label = "View beatmap",
|
||||
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
||||
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ namespace osu.Desktop.Security
|
||||
|
||||
private partial class ElevatedPrivilegesNotification : SimpleNotification
|
||||
{
|
||||
public override bool IsImportant => true;
|
||||
|
||||
public ElevatedPrivilegesNotification()
|
||||
{
|
||||
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1053" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -12,7 +12,6 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
@@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
protected override void UpdatePlacementTimeAndPosition()
|
||||
{
|
||||
var result = base.SnapForBlueprint(blueprint);
|
||||
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
||||
return result;
|
||||
var position = InputManager.CurrentState.Mouse.Position;
|
||||
double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP;
|
||||
CurrentBlueprint.UpdateTimeAndPosition(position, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Edit.Setup;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
@@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new DifficultySection(),
|
||||
new CatchDifficultySection(),
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
@@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
if (!(result.Time is double time)) return;
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
|
||||
if (!(result.Time is double time)) return result;
|
||||
|
||||
switch (PlacementActive)
|
||||
{
|
||||
@@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
|
||||
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
public abstract partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
where THitObject : CatchHitObject, new()
|
||||
{
|
||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||
@@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; } = null!;
|
||||
|
||||
public CatchPlacementBlueprint()
|
||||
[Resolved]
|
||||
protected CatchHitObjectComposer? Composer { get; private set; }
|
||||
|
||||
protected CatchPlacementBlueprint()
|
||||
: base(new THitObject())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
@@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||
|
||||
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||
? distanceSnapResult
|
||||
: gridSnapResult;
|
||||
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
|
||||
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||
|
||||
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||
? distanceSnapResult
|
||||
: gridSnapResult;
|
||||
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
@@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Make sure the up-to-date position is used for outlines.
|
||||
@@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
ApplyDefaultsToHitObject();
|
||||
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
||||
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
||||
return result;
|
||||
}
|
||||
|
||||
private double positionToTime(float relativeYPosition)
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public partial class CatchBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
|
||||
|
||||
public CatchBlueprintContainer(CatchHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
@@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
}
|
||||
|
||||
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
||||
|
||||
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||
{
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition);
|
||||
gridSnapResult.ScreenSpacePosition.X = movePosition.X;
|
||||
var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||
|
||||
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||
? distanceSnapResult
|
||||
: gridSnapResult;
|
||||
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||
if (moved)
|
||||
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||
return moved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
//
|
||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
|
||||
|
||||
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
|
||||
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
|
||||
|
||||
@@ -23,9 +23,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private const float distance_snap_radius = 50;
|
||||
public const float DISTANCE_SNAP_RADIUS = 50;
|
||||
|
||||
private CatchDistanceSnapGrid distanceSnapGrid = null!;
|
||||
|
||||
@@ -135,22 +136,12 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult)
|
||||
return snapResult;
|
||||
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (snapType.HasFlag(SnapType.RelativeGrids))
|
||||
{
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
{
|
||||
result = snapResult;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using Direction = osu.Framework.Graphics.Direction;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
@@ -38,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
return true;
|
||||
}
|
||||
|
||||
moveSelection(deltaX);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void moveSelection(float deltaX)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (!(h is CatchHitObject catchObject)) return;
|
||||
@@ -48,7 +57,60 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
foreach (var nested in catchObject.NestedHitObjects.OfType<CatchHitObject>())
|
||||
nested.OriginalX += deltaX;
|
||||
});
|
||||
}
|
||||
|
||||
private bool nudgeMovementActive;
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// Until the keys below are global actions, this will prevent conflicts with "seek between sample points"
|
||||
// which has a default of ctrl+shift+arrows.
|
||||
if (e.ShiftPressed)
|
||||
return false;
|
||||
|
||||
if (e.ControlPressed)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left:
|
||||
return nudgeSelection(-1);
|
||||
|
||||
case Key.Right:
|
||||
return nudgeSelection(1);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
if (nudgeMovementActive && !e.ControlPressed)
|
||||
{
|
||||
EditorBeatmap.EndChange();
|
||||
nudgeMovementActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints).
|
||||
/// </summary>
|
||||
private bool nudgeSelection(float deltaX)
|
||||
{
|
||||
if (!nudgeMovementActive)
|
||||
{
|
||||
nudgeMovementActive = true;
|
||||
EditorBeatmap.BeginChange();
|
||||
}
|
||||
|
||||
var firstBlueprint = SelectedBlueprints.FirstOrDefault();
|
||||
|
||||
if (firstBlueprint == null)
|
||||
return false;
|
||||
|
||||
moveSelection(deltaX);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
{
|
||||
public partial class CatchDifficultySection : SetupSection
|
||||
{
|
||||
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
circleSizeSlider = new FormSliderBar<float>
|
||||
{
|
||||
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 FormSliderBar<float>
|
||||
{
|
||||
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 FormSliderBar<float>
|
||||
{
|
||||
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,
|
||||
},
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
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 FormSliderBar<double>
|
||||
{
|
||||
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<FormSliderBar<float>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
|
||||
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
private void updateValues()
|
||||
{
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
|
||||
if (keyCounter != null)
|
||||
{
|
||||
@@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
keyCounter.Origin = Anchor.TopRight;
|
||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||
}
|
||||
|
||||
if (spectatorList != null)
|
||||
{
|
||||
spectatorList.Anchor = Anchor.BottomLeft;
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
spectatorList.Position = new Vector2(10, -10);
|
||||
}
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new LegacyKeyCounterDisplay(),
|
||||
new SpectatorList(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
|
||||
private readonly Container scaleContainer;
|
||||
|
||||
public CatchPlayfieldAdjustmentContainer()
|
||||
{
|
||||
const float base_game_width = 1024f;
|
||||
@@ -26,30 +29,49 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = new Container
|
||||
InternalChild = scaleContainer = new Container
|
||||
{
|
||||
// This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
|
||||
// Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
|
||||
Name = "Visible area",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = base_game_height + extra_bottom_space,
|
||||
Y = extra_bottom_space / 2,
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
Name = "Playable area",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
|
||||
Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
// This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
|
||||
// Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
|
||||
Name = "Visible area",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = base_game_height + extra_bottom_space,
|
||||
Y = extra_bottom_space / 2,
|
||||
Masking = true,
|
||||
Child = new Container
|
||||
{
|
||||
Name = "Playable area",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
|
||||
Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGame? osuGame)
|
||||
{
|
||||
if (osuGame != null)
|
||||
{
|
||||
// on mobile platforms where the base aspect ratio is wider, the catch playfield
|
||||
// needs to be scaled down to remain playable.
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> which scales its content relative to a target width.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,7 +9,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
});
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
protected override void UpdatePlacementTimeAndPosition()
|
||||
{
|
||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
var pos = column.ScreenSpacePositionAtTime(time);
|
||||
|
||||
return new SnapResult(pos, time, column);
|
||||
CurrentBlueprint.UpdateTimeAndPosition(pos, time);
|
||||
}
|
||||
|
||||
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
@@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
set => InternalChild = value;
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
}
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
|
||||
|
||||
@@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Child = new ColumnHitObjectArea(new HitObjectContainer())
|
||||
Child = new ColumnHitObjectArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new HitObjectContainer(),
|
||||
}
|
||||
},
|
||||
new ColumnTestContainer(1, ManiaAction.Key2)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Child = new ColumnHitObjectArea(new HitObjectContainer())
|
||||
Child = new ColumnHitObjectArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new HitObjectContainer(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneManiaTouchInput : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestTouchInput()
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
||||
AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(index).Action.Value));
|
||||
|
||||
AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(index).Action.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOneColumnMultipleTouches()
|
||||
{
|
||||
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action still pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action still pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestTouchAreaNotInitiallyVisible()
|
||||
{
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPressReceptors()
|
||||
{
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
||||
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(index).Action.Value));
|
||||
|
||||
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||
|
||||
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
|
||||
private double originalStartTime;
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime);
|
||||
|
||||
if (PlacementActive == PlacementState.Active)
|
||||
{
|
||||
@@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
if (result.Time is double startTime)
|
||||
originalStartTime = HitObject.StartTime = startTime;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
@@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||
private ManiaHitObjectComposer? positionSnapProvider { get; set; }
|
||||
|
||||
private EditBodyPiece body = null!;
|
||||
private EditHoldNoteEndPiece head = null!;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
protected new T HitObject => (T)base.HitObject;
|
||||
|
||||
private Column column;
|
||||
[Resolved]
|
||||
private ManiaHitObjectComposer? composer { get; set; }
|
||||
|
||||
public Column Column
|
||||
private Column? column;
|
||||
|
||||
public Column? Column
|
||||
{
|
||||
get => column;
|
||||
set
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
if (value == column)
|
||||
return;
|
||||
|
||||
@@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
|
||||
if (result.Playfield is Column col)
|
||||
{
|
||||
@@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = col;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private float getNoteHeight(Column resultPlayfield) =>
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
@@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
};
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime);
|
||||
|
||||
if (result.Playfield != null)
|
||||
{
|
||||
piece.Width = result.Playfield.DrawWidth;
|
||||
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public partial class ManiaBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public ManiaBlueprintContainer(HitObjectComposer composer)
|
||||
public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer;
|
||||
|
||||
public ManiaBlueprintContainer(ManiaHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
}
|
||||
@@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
|
||||
|
||||
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
||||
|
||||
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||
{
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = Composer.FindSnappedPositionAndTime(movePosition);
|
||||
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||
if (moved)
|
||||
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||
return moved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
||||
{
|
||||
private DrawableManiaEditorRuleset drawableRuleset = null!;
|
||||
@@ -64,11 +65,11 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
return;
|
||||
|
||||
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||
string[] objectDescriptions = objectDescription.Split(',').ToArray();
|
||||
string[] objectDescriptions = objectDescription.Split(',');
|
||||
|
||||
for (int i = 0; i < objectDescriptions.Length; i++)
|
||||
{
|
||||
string[] split = objectDescriptions[i].Split('|').ToArray();
|
||||
string[] split = objectDescriptions[i].Split('|');
|
||||
if (split.Length != 2)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
[Cached] // Used for touch input, see ColumnTouchInputArea.
|
||||
[Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp.
|
||||
public partial class ManiaInputManager : RulesetInputManager<ManiaAction>
|
||||
{
|
||||
public ManiaInputManager(RulesetInfo ruleset, int variant)
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
|
||||
{
|
||||
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
|
||||
HitObjectContainer hoc = column.HitObjectContainer;
|
||||
Container hocParent = (Container)hoc.Parent!;
|
||||
|
||||
hocParent.Remove(hoc, false);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -12,6 +13,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -19,25 +21,29 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||
{
|
||||
private const float judgement_y_position = 160;
|
||||
private const float judgement_y_position = -180f;
|
||||
|
||||
private RingExplosion? ringExplosion;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private IBindable<ScrollingDirection> direction = null!;
|
||||
|
||||
public ArgonJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
Y = judgement_y_position;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
|
||||
if (Result.IsHit())
|
||||
{
|
||||
AddInternal(ringExplosion = new RingExplosion(Result)
|
||||
@@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
}
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
|
||||
protected override SpriteText CreateJudgementText() =>
|
||||
new OsuSpriteText
|
||||
{
|
||||
@@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
|
||||
@@ -9,7 +9,9 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
@@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
@@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
combo.Origin = Anchor.Centre;
|
||||
combo.Y = 200;
|
||||
}
|
||||
|
||||
if (spectatorList != null)
|
||||
spectatorList.Position = new Vector2(36, -66);
|
||||
})
|
||||
{
|
||||
new ArgonManiaComboCounter(),
|
||||
new SpectatorList
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
@@ -23,21 +24,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
this.result = result;
|
||||
this.animation = animation;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
private IBindable<ScrollingDirection> direction = null!;
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
float? scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value;
|
||||
|
||||
if (scorePosition != null)
|
||||
scorePosition -= Stage.HIT_TARGET_POSITION + 150;
|
||||
|
||||
Y = scorePosition ?? 0;
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
|
||||
InternalChild = animation.With(d =>
|
||||
{
|
||||
@@ -46,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
});
|
||||
}
|
||||
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
|
||||
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
|
||||
|
||||
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
float finalPosition = scorePosition - absoluteHitPosition;
|
||||
|
||||
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
|
||||
}
|
||||
|
||||
public void PlayAnimation()
|
||||
{
|
||||
(animation as IFramedAnimation)?.GotoFrame(0);
|
||||
|
||||
@@ -15,7 +15,9 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
@@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
@@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
combo.Origin = Anchor.Centre;
|
||||
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
|
||||
}
|
||||
|
||||
if (spectatorList != null)
|
||||
{
|
||||
spectatorList.Anchor = Anchor.BottomLeft;
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
spectatorList.Position = new Vector2(10, -10);
|
||||
}
|
||||
})
|
||||
{
|
||||
new LegacyManiaComboCounter(),
|
||||
new SpectatorList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
@@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
||||
private DrawablePool<PoolableHitExplosion> hitExplosionPool = null!;
|
||||
private readonly OrderedHitPolicy hitPolicy;
|
||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||
|
||||
private GameplaySampleTriggerSource sampleTriggerSource;
|
||||
private GameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a special (ie. scratch) column.
|
||||
@@ -67,11 +66,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Width = COLUMN_WIDTH;
|
||||
|
||||
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
||||
HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both };
|
||||
HitObjectArea = new ColumnHitObjectArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = HitObjectContainer,
|
||||
};
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
@@ -132,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin != null)
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= onSourceChanged;
|
||||
}
|
||||
|
||||
@@ -180,5 +183,29 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
||||
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
|
||||
#region Touch Input
|
||||
|
||||
[Resolved]
|
||||
private ManiaInputManager? maniaInputManager { get; set; }
|
||||
|
||||
private int touchActivationCount;
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
|
||||
touchActivationCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnTouchUp(TouchUpEvent e)
|
||||
{
|
||||
touchActivationCount--;
|
||||
|
||||
if (touchActivationCount == 0)
|
||||
maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
{
|
||||
public partial class ColumnHitObjectArea : HitObjectArea
|
||||
public partial class ColumnHitObjectArea : HitPositionPaddedContainer
|
||||
{
|
||||
public readonly Container Explosions;
|
||||
|
||||
@@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
|
||||
private readonly Drawable hitTarget;
|
||||
|
||||
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
|
||||
: base(hitObjectContainer)
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private readonly Container content;
|
||||
|
||||
public ColumnHitObjectArea()
|
||||
{
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
UnderlayElements = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 2,
|
||||
},
|
||||
hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = 1
|
||||
},
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
Explosions = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = -1,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+18
-24
@@ -1,52 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
{
|
||||
public partial class HitObjectArea : SkinReloadableDrawable
|
||||
public partial class HitPositionPaddedContainer : Container
|
||||
{
|
||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
public readonly HitObjectContainer HitObjectContainer;
|
||||
|
||||
public HitObjectArea(HitObjectContainer hitObjectContainer)
|
||||
{
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = HitObjectContainer = hitObjectContainer
|
||||
};
|
||||
}
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
Direction.BindTo(scrollingInfo.Direction);
|
||||
Direction.BindValueChanged(onDirectionChanged, true);
|
||||
Direction.BindValueChanged(_ => UpdateHitPosition(), true);
|
||||
|
||||
skin.SourceChanged += onSkinChanged;
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
base.SkinChanged(skin);
|
||||
UpdateHitPosition();
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
UpdateHitPosition();
|
||||
}
|
||||
private void onSkinChanged() => UpdateHitPosition();
|
||||
|
||||
protected virtual void UpdateHitPosition()
|
||||
{
|
||||
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
|
||||
?? Stage.HIT_TARGET_POSITION;
|
||||
|
||||
@@ -54,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
? new MarginPadding { Top = hitPosition }
|
||||
: new MarginPadding { Bottom = hitPosition };
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||
{
|
||||
private const float judgement_y_position = -180f;
|
||||
|
||||
private IBindable<ScrollingDirection> direction = null!;
|
||||
|
||||
public DefaultManiaJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
JudgementText.Font = JudgementText.Font.With(size: 25);
|
||||
}
|
||||
|
||||
public override void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case HitResult.None:
|
||||
this.FadeOutFromOne(800);
|
||||
break;
|
||||
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.ScaleTo(0.8f);
|
||||
this.ScaleTo(1, 250, Easing.OutElastic);
|
||||
|
||||
this.Delay(50)
|
||||
.ScaleTo(0.75f, 250)
|
||||
.FadeOut(200);
|
||||
|
||||
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,51 +3,32 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public partial class DrawableManiaJudgement : DrawableJudgement
|
||||
{
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||
private IBindable<ScrollingDirection> direction;
|
||||
|
||||
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
public DefaultManiaJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
JudgementText.Font = JudgementText.Font.With(size: 25);
|
||||
}
|
||||
|
||||
public override void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case HitResult.None:
|
||||
case HitResult.Miss:
|
||||
base.PlayAnimation();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.ScaleTo(0.8f);
|
||||
this.ScaleTo(1, 250, Easing.OutElastic);
|
||||
|
||||
this.Delay(50)
|
||||
.ScaleTo(0.75f, 250)
|
||||
.FadeOut(200);
|
||||
|
||||
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
|
||||
break;
|
||||
}
|
||||
}
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
[Cached]
|
||||
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public IEnumerable<BarLine> BarLines;
|
||||
|
||||
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
|
||||
|
||||
protected override bool RelativeScaleBeatLengths => true;
|
||||
|
||||
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
|
||||
@@ -110,8 +111,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
||||
|
||||
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||
|
||||
KeyBindingInputManager.Add(new ManiaTouchInputArea());
|
||||
}
|
||||
|
||||
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
||||
@@ -162,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <returns>The scroll time.</returns>
|
||||
public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this);
|
||||
|
||||
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
|
||||
|
||||
|
||||
@@ -1,17 +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 System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
|
||||
{
|
||||
public ManiaPlayfieldAdjustmentContainer()
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
private readonly DrawSizePreservingFillContainer scalingContainer;
|
||||
|
||||
private readonly DrawableManiaRuleset drawableManiaRuleset;
|
||||
|
||||
public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset)
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
this.drawableManiaRuleset = drawableManiaRuleset;
|
||||
InternalChild = scalingContainer = new DrawSizePreservingFillContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = Content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
float aspectRatio = DrawWidth / DrawHeight;
|
||||
bool isPortrait = aspectRatio < 1f;
|
||||
|
||||
if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1)
|
||||
{
|
||||
// Scale playfield up by 25% to become playable on mobile devices,
|
||||
// and leave a 10% horizontal gap if the playfield is scaled down due to being too wide.
|
||||
const float base_scale = 1.25f;
|
||||
const float base_width = 768f / base_scale;
|
||||
const float side_gap = 0.9f;
|
||||
|
||||
scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum;
|
||||
float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth;
|
||||
scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f));
|
||||
}
|
||||
else
|
||||
{
|
||||
scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum;
|
||||
scalingContainer.Scale = new Vector2(1f);
|
||||
scalingContainer.Size = new Vector2(1f);
|
||||
scalingContainer.TargetDrawSize = new Vector2(1024, 768);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An overlay that captures and displays osu!mania mouse and touch input.
|
||||
/// </summary>
|
||||
public partial class ManiaTouchInputArea : VisibilityContainer
|
||||
{
|
||||
// visibility state affects our child. we always want to handle input.
|
||||
public override bool PropagatePositionalInputSubTree => true;
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
[SettingSource("Spacing", "The spacing between receptors.")]
|
||||
public BindableFloat Spacing { get; } = new BindableFloat(10)
|
||||
{
|
||||
Precision = 1,
|
||||
MinValue = 0,
|
||||
MaxValue = 100,
|
||||
};
|
||||
|
||||
[SettingSource("Opacity", "The receptor opacity.")]
|
||||
public BindableFloat Opacity { get; } = new BindableFloat(1)
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1
|
||||
};
|
||||
|
||||
[Resolved]
|
||||
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
|
||||
|
||||
private GridContainer gridContainer = null!;
|
||||
|
||||
public ManiaTouchInputArea()
|
||||
{
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Height = 0.5f;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
List<Drawable> receptorGridContent = new List<Drawable>();
|
||||
List<Dimension> receptorGridDimensions = new List<Dimension>();
|
||||
|
||||
bool first = true;
|
||||
|
||||
foreach (var stage in drawableRuleset.Playfield.Stages)
|
||||
{
|
||||
foreach (var column in stage.Columns)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
|
||||
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
|
||||
}
|
||||
|
||||
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
|
||||
receptorGridDimensions.Add(new Dimension());
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
InternalChild = gridContainer = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Content = new[] { receptorGridContent.ToArray() },
|
||||
ColumnDimensions = receptorGridDimensions.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// Hide whenever the keyboard is used.
|
||||
Hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
Show();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
gridContainer.FadeIn(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
gridContainer.FadeOut(300);
|
||||
}
|
||||
|
||||
public partial class ColumnInputReceptor : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
[Resolved]
|
||||
private ManiaInputManager? inputManager { get; set; }
|
||||
|
||||
private bool isPressed;
|
||||
|
||||
public ColumnInputReceptor()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.15f,
|
||||
},
|
||||
highlightOverlay = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
updateButton(true);
|
||||
return false; // handled by parent container to show overlay.
|
||||
}
|
||||
|
||||
protected override void OnTouchUp(TouchUpEvent e)
|
||||
{
|
||||
updateButton(false);
|
||||
}
|
||||
|
||||
private void updateButton(bool press)
|
||||
{
|
||||
if (press == isPressed)
|
||||
return;
|
||||
|
||||
isPressed = press;
|
||||
|
||||
if (press)
|
||||
{
|
||||
inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
|
||||
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
|
||||
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class Gutter : Drawable
|
||||
{
|
||||
public readonly IBindable<float> Spacing = new Bindable<float>();
|
||||
|
||||
public Gutter()
|
||||
{
|
||||
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Width = 1366, // Bar lines should only be masked on the vertical axis
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = barLineContainer = new HitObjectArea(HitObjectContainer)
|
||||
Child = barLineContainer = new HitPositionPaddedContainer
|
||||
{
|
||||
Name = "Bar lines",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Child = HitObjectContainer,
|
||||
}
|
||||
},
|
||||
columnFlow = new ColumnFlow<Column>(definition)
|
||||
@@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
judgements = new JudgementContainer<DrawableManiaJudgement>
|
||||
new HitPositionPaddedContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Y = HIT_TARGET_POSITION + 150
|
||||
Child = judgements = new JudgementContainer<DrawableManiaJudgement>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||
}
|
||||
@@ -214,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
return;
|
||||
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPooler.Get(result.Type, j =>
|
||||
{
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
})!);
|
||||
judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -24,11 +24,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissTail() => CreateModTest(new ModTestData
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void TestMissTail(bool tailMiss) => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Mod = new OsuModSuddenDeath
|
||||
{
|
||||
FailOnSliderTail = { Value = tailMiss }
|
||||
},
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(tailMiss),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
public partial class GridPlacementBlueprint : PlacementBlueprint
|
||||
{
|
||||
[Resolved]
|
||||
private HitObjectComposer? hitObjectComposer { get; set; }
|
||||
private OsuHitObjectComposer? hitObjectComposer { get; set; }
|
||||
|
||||
private OsuGridToolboxGroup gridToolboxGroup = null!;
|
||||
private Vector2 originalOrigin;
|
||||
@@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
public override SnapType SnapType => ~SnapType.GlobalGrids;
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
if (State.Value == Visibility.Hidden)
|
||||
return;
|
||||
return new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
var pos = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
@@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
@@ -15,12 +20,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
|
||||
private readonly HitCirclePiece circlePiece;
|
||||
|
||||
[Resolved]
|
||||
private OsuHitObjectComposer? composer { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock? editorClock { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
public HitCirclePlacementBlueprint()
|
||||
: base(new HitCircle())
|
||||
{
|
||||
InternalChild = circlePiece = new HitCirclePiece();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@@ -45,10 +64,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
|
||||
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-6
@@ -9,6 +9,7 @@ using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -20,6 +21,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -32,7 +34,7 @@ using osuTK.Input;
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
where T : OsuHitObject, IHasPath
|
||||
where T : OsuHitObject, IHasPath, IHasSliderVelocity
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
|
||||
|
||||
@@ -48,11 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public Action<List<PathControlPoint>> SplitControlPointsRequested;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPositionSnapProvider positionSnapProvider { get; set; }
|
||||
[CanBeNull]
|
||||
private OsuHitObjectComposer positionSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
public PathControlPointVisualiser(T hitObject, bool allowSelection)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
@@ -67,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@@ -433,12 +444,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
// 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]));
|
||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||
var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime);
|
||||
result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null);
|
||||
if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(newHeadPosition, oldStartTime);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position;
|
||||
|
||||
hitObject.Position += movementDelta;
|
||||
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
|
||||
hitObject.StartTime = result.Time ?? hitObject.StartTime;
|
||||
|
||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
@@ -453,7 +469,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||
SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition));
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -25,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
[Resolved]
|
||||
private OsuHitObjectComposer? composer { get; set; }
|
||||
|
||||
private SliderBodyPiece bodyPiece = null!;
|
||||
private HitCirclePiece headCirclePiece = null!;
|
||||
private HitCirclePiece tailCirclePiece = null!;
|
||||
@@ -40,15 +45,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private int currentSegmentLength;
|
||||
private bool usingCustomSegmentType;
|
||||
|
||||
[Resolved]
|
||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock? editorClock { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
@@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
};
|
||||
|
||||
state = SliderPlacementState.Initial;
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -106,9 +114,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
|
||||
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
@@ -131,6 +145,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
updateCursor();
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
@@ -375,7 +391,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private Vector2 getCursorPosition()
|
||||
{
|
||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
|
||||
SnapResult? result = null;
|
||||
var mousePosition = inputManager.CurrentState.Mouse.Position;
|
||||
|
||||
if (state != SliderPlacementState.ControlPoints)
|
||||
{
|
||||
result ??= composer?.TrySnapToNearbyObjects(mousePosition);
|
||||
result ??= composer?.TrySnapToDistanceGrid(mousePosition);
|
||||
}
|
||||
|
||||
result ??= composer?.TrySnapToPositionGrid(mousePosition);
|
||||
|
||||
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
}
|
||||
|
||||
@@ -408,7 +434,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, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
||||
bodyPiece.UpdateFrom(HitObject);
|
||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||
|
||||
@@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
else
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * 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, DistanceSnapTarget.Start) ?? proposedDistance;
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
}
|
||||
|
||||
@@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0)
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0))
|
||||
return true;
|
||||
|
||||
if (ControlPointVisualiser == null)
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||
@@ -8,16 +14,27 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public OsuBlueprintContainer(HitObjectComposer composer)
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer;
|
||||
|
||||
public OsuBlueprintContainer(OsuHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
|
||||
|
||||
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||
@@ -36,5 +53,68 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
return base.CreateHitObjectBlueprintFor(hitObject);
|
||||
}
|
||||
|
||||
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||
{
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
for (int i = 0; i < blueprints.Count; i++)
|
||||
{
|
||||
if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions))
|
||||
return true;
|
||||
}
|
||||
|
||||
// if no positional snapping could be performed, try unrestricted snapping from the earliest
|
||||
// item in the selection.
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = Composer.TrySnapToNearbyObjects(movePosition);
|
||||
result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null);
|
||||
if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(movePosition, null);
|
||||
|
||||
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||
if (moved)
|
||||
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||
return moved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for positional snap for given blueprint.
|
||||
/// </summary>
|
||||
/// <param name="blueprint">The blueprint to check for snapping.</param>
|
||||
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
|
||||
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
|
||||
/// <returns>Whether an object to snap to was found.</returns>
|
||||
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<HitObject> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
|
||||
{
|
||||
var currentPositions = blueprint.ScreenSpaceSnapPoints;
|
||||
|
||||
for (int i = 0; i < originalPositions.Length; i++)
|
||||
{
|
||||
Vector2 originalPosition = originalPositions[i];
|
||||
var testPosition = originalPosition + distanceTravelled;
|
||||
|
||||
var positionalResult = Composer.TrySnapToNearbyObjects(testPosition);
|
||||
|
||||
if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||
|
||||
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
|
||||
|
||||
// attempt to move the objects, and apply any time based snapping if we can.
|
||||
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(blueprint, delta)))
|
||||
{
|
||||
ApplySnapResultTime(positionalResult, blueprint.Item.StartTime);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
@@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
||||
{
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
|
||||
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1)
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null)
|
||||
: base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource)
|
||||
{
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@@ -14,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType<IHasSliderVelocity>().LastOrDefault();
|
||||
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity);
|
||||
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
|
||||
@@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
@@ -22,6 +23,7 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -31,6 +33,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
|
||||
{
|
||||
public OsuHitObjectComposer(Ruleset ruleset)
|
||||
@@ -178,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return;
|
||||
|
||||
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||
string[] splitDescription = objectDescription.Split(',').ToArray();
|
||||
string[] splitDescription = objectDescription.Split(',');
|
||||
|
||||
for (int i = 0; i < splitDescription.Length; i++)
|
||||
{
|
||||
@@ -222,56 +225,56 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
[CanBeNull]
|
||||
public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null)
|
||||
{
|
||||
if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
{
|
||||
// In the case of snapping to nearby objects, a time value is not provided.
|
||||
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
|
||||
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
|
||||
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
|
||||
//
|
||||
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
|
||||
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
|
||||
// the time value if the proposed positions are roughly the same.
|
||||
if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
|
||||
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
|
||||
snapResult.Time = distanceSnappedTime;
|
||||
}
|
||||
if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
return null;
|
||||
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||
return snapResult;
|
||||
}
|
||||
|
||||
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
// In the case of snapping to nearby objects, a time value is not provided.
|
||||
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
|
||||
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
|
||||
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
|
||||
//
|
||||
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
|
||||
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
|
||||
// the time value if the proposed positions are roughly the same.
|
||||
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
|
||||
snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)
|
||||
? distanceSnappedTime
|
||||
: fallbackTime;
|
||||
|
||||
if (snapType.HasFlag(SnapType.RelativeGrids))
|
||||
{
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return snapResult;
|
||||
}
|
||||
|
||||
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
|
||||
result.Time = time;
|
||||
}
|
||||
}
|
||||
[CanBeNull]
|
||||
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null)
|
||||
{
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||
return null;
|
||||
|
||||
if (snapType.HasFlag(SnapType.GlobalGrids))
|
||||
{
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
||||
}
|
||||
|
||||
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
|
||||
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
|
||||
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||
[CanBeNull]
|
||||
public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null)
|
||||
{
|
||||
if (rectangularGridSnapToggle.Value != TernaryState.True)
|
||||
return null;
|
||||
|
||||
result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos);
|
||||
}
|
||||
}
|
||||
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
|
||||
return result;
|
||||
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
|
||||
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
|
||||
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
return new SnapResult(positionSnapGrid.ToScreenSpace(pos), fallbackTime, playfield);
|
||||
}
|
||||
|
||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||
@@ -404,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
|
||||
|
||||
int sourceIndex = -1;
|
||||
int positionSourceObjectIndex = -1;
|
||||
IHasSliderVelocity sliderVelocitySource = null;
|
||||
|
||||
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
|
||||
{
|
||||
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
|
||||
break;
|
||||
|
||||
sourceIndex = i;
|
||||
positionSourceObjectIndex = i;
|
||||
|
||||
if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity)
|
||||
sliderVelocitySource = hasSliderVelocity;
|
||||
}
|
||||
|
||||
if (sourceIndex == -1)
|
||||
if (positionSourceObjectIndex == -1)
|
||||
return null;
|
||||
|
||||
HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
|
||||
HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex];
|
||||
|
||||
int targetIndex = sourceIndex + targetOffset;
|
||||
int targetIndex = positionSourceObjectIndex + targetOffset;
|
||||
HitObject targetObject = null;
|
||||
|
||||
// Keep advancing the target object while its start time falls before the end time of the source object
|
||||
@@ -440,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (sourceObject is Spinner)
|
||||
return null;
|
||||
|
||||
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject);
|
||||
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||
}
|
||||
|
||||
private bool nudgeMovementActive;
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
||||
@@ -48,9 +50,43 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return true;
|
||||
}
|
||||
|
||||
// Until the keys below are global actions, this will prevent conflicts with "seek between sample points"
|
||||
// which has a default of ctrl+shift+arrows.
|
||||
if (e.ShiftPressed)
|
||||
return false;
|
||||
|
||||
if (e.ControlPressed)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left:
|
||||
return nudgeSelection(new Vector2(-1, 0));
|
||||
|
||||
case Key.Right:
|
||||
return nudgeSelection(new Vector2(1, 0));
|
||||
|
||||
case Key.Up:
|
||||
return nudgeSelection(new Vector2(0, -1));
|
||||
|
||||
case Key.Down:
|
||||
return nudgeSelection(new Vector2(0, 1));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
if (nudgeMovementActive && !e.ControlPressed)
|
||||
{
|
||||
EditorBeatmap.EndChange();
|
||||
nudgeMovementActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
@@ -70,6 +106,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
|
||||
return true;
|
||||
|
||||
moveObjects(hitObjects, localDelta);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void moveObjects(OsuHitObject[] hitObjects, Vector2 localDelta)
|
||||
{
|
||||
// this will potentially move the selection out of bounds...
|
||||
foreach (var h in hitObjects)
|
||||
h.Position += localDelta;
|
||||
@@ -81,7 +124,26 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
|
||||
// as the entire flow is too expensive to run on every movement.
|
||||
Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints).
|
||||
/// </summary>
|
||||
/// <param name="delta"></param>
|
||||
private bool nudgeSelection(Vector2 delta)
|
||||
{
|
||||
if (!nudgeMovementActive)
|
||||
{
|
||||
nudgeMovementActive = true;
|
||||
EditorBeatmap.BeginChange();
|
||||
}
|
||||
|
||||
var firstBlueprint = SelectedBlueprints.FirstOrDefault();
|
||||
|
||||
if (firstBlueprint == null)
|
||||
return false;
|
||||
|
||||
moveObjects(selectedMovableObjects, delta);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class PreciseMovementPopover : OsuPopover
|
||||
{
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
private readonly Dictionary<HitObject, Vector2> initialPositions = new Dictionary<HitObject, Vector2>();
|
||||
private RectangleF initialSurroundingQuad;
|
||||
|
||||
private BindableNumber<float> xBindable = null!;
|
||||
private BindableNumber<float> yBindable = null!;
|
||||
|
||||
private SliderWithTextBoxInput<float> xInput = null!;
|
||||
private OsuCheckbox relativeCheckbox = null!;
|
||||
|
||||
public PreciseMovementPopover()
|
||||
{
|
||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
xInput = new SliderWithTextBoxInput<float>("X:")
|
||||
{
|
||||
Current = xBindable = new BindableNumber<float>
|
||||
{
|
||||
Precision = 1,
|
||||
},
|
||||
Instantaneous = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new SliderWithTextBoxInput<float>("Y:")
|
||||
{
|
||||
Current = yBindable = new BindableNumber<float>
|
||||
{
|
||||
Precision = 1,
|
||||
},
|
||||
Instantaneous = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
relativeCheckbox = new OsuCheckbox(false)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
LabelText = "Relative movement",
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() => xInput.TakeFocus());
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
editorBeatmap.BeginChange();
|
||||
initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair<HitObject, Vector2>(ho, ((IHasPosition)ho).Position)));
|
||||
initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast<IHasPosition>()).AABBFloat;
|
||||
|
||||
Debug.Assert(initialPositions.Count > 0);
|
||||
|
||||
if (initialPositions.Count > 1)
|
||||
{
|
||||
relativeCheckbox.Current.Value = true;
|
||||
relativeCheckbox.Current.Disabled = true;
|
||||
}
|
||||
|
||||
relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true);
|
||||
xBindable.BindValueChanged(_ => applyPosition());
|
||||
yBindable.BindValueChanged(_ => applyPosition());
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
if (IsLoaded) editorBeatmap.EndChange();
|
||||
}
|
||||
|
||||
private void relativeChanged()
|
||||
{
|
||||
// reset bindable bounds to something that is guaranteed to be larger than any previous value.
|
||||
// this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic -
|
||||
// if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue.
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||
|
||||
float previousX = xBindable.Value;
|
||||
float previousY = yBindable.Value;
|
||||
|
||||
if (relativeCheckbox.Current.Value)
|
||||
{
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
|
||||
|
||||
xBindable.Default = yBindable.Default = 0;
|
||||
|
||||
if (initialPositions.Count == 1)
|
||||
{
|
||||
var initialPosition = initialPositions.Single().Value;
|
||||
xBindable.Value = previousX - initialPosition.X;
|
||||
yBindable.Value = previousY - initialPosition.Y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(initialPositions.Count == 1);
|
||||
var initialPosition = initialPositions.Single().Value;
|
||||
|
||||
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
|
||||
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
|
||||
|
||||
xBindable.Default = initialPosition.X;
|
||||
yBindable.Default = initialPosition.Y;
|
||||
|
||||
xBindable.Value = xBindable.Default + previousX;
|
||||
yBindable.Value = yBindable.Default + previousY;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPosition()
|
||||
{
|
||||
editorBeatmap.PerformOnSelection(ho =>
|
||||
{
|
||||
if (!initialPositions.TryGetValue(ho, out var initialPosition))
|
||||
return;
|
||||
|
||||
var pos = new Vector2(xBindable.Value, yBindable.Value);
|
||||
if (relativeCheckbox.Current.Value)
|
||||
((IHasPosition)ho).Position = initialPosition + pos;
|
||||
else
|
||||
((IHasPosition)ho).Position = pos;
|
||||
});
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Action == GlobalAction.Select && !e.Repeat)
|
||||
{
|
||||
this.HidePopover();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
angleInput.TakeFocus();
|
||||
angleInput.SelectAll();
|
||||
});
|
||||
ScheduleAfterChildren(() => angleInput.TakeFocus());
|
||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||
|
||||
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||
|
||||
@@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
scaleInput.TakeFocus();
|
||||
scaleInput.SelectAll();
|
||||
});
|
||||
ScheduleAfterChildren(() => scaleInput.TakeFocus());
|
||||
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||
|
||||
xCheckBox.Current.BindValueChanged(_ =>
|
||||
|
||||
@@ -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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
@@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
||||
private readonly BindableBool canMove = new BindableBool();
|
||||
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
||||
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
||||
|
||||
private EditorToolButton moveButton = null!;
|
||||
private EditorToolButton rotateButton = null!;
|
||||
private EditorToolButton scaleButton = null!;
|
||||
|
||||
@@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
@@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
moveButton = new EditorToolButton("Move",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||
() => new PreciseMovementPopover()),
|
||||
rotateButton = new EditorToolButton("Rotate",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
||||
scaleButton = new EditorToolButton("Scale",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt },
|
||||
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
||||
}
|
||||
};
|
||||
|
||||
selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true);
|
||||
|
||||
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
||||
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
||||
|
||||
@@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||
canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true);
|
||||
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
||||
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
||||
}
|
||||
@@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorToggleMoveControl:
|
||||
{
|
||||
moveButton.TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
case GlobalAction.EditorToggleRotateControl:
|
||||
{
|
||||
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -63,13 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
|
||||
{
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
|
||||
new ApproachRateSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = current,
|
||||
KeyboardStep = 0.1f,
|
||||
};
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) => new ApproachRateSlider();
|
||||
|
||||
/// <summary>
|
||||
/// A slider bar with more detailed approach rate info for its given value
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override LocalisableString Description => "Flip objects on the chosen axes.";
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
|
||||
|
||||
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
|
||||
[SettingSource("Flipped axes")]
|
||||
public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>();
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@@ -13,5 +18,19 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
typeof(OsuModTargetPractice),
|
||||
}).ToArray();
|
||||
|
||||
[SettingSource("Also fail when missing a slider tail")]
|
||||
public BindableBool FailOnSliderTail { get; } = new BindableBool();
|
||||
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
{
|
||||
if (base.FailCondition(healthProcessor, result))
|
||||
return true;
|
||||
|
||||
if (FailOnSliderTail.Value && result.HitObject is SliderTailCircle && !result.IsHit)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Slider slider = drawableSlider.HitObject;
|
||||
Position = slider.CurvePositionAt(completionProgress);
|
||||
|
||||
//0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
|
||||
var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance));
|
||||
// 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
|
||||
double checkDistance = 0.1 / slider.Path.Distance;
|
||||
var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance));
|
||||
|
||||
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
||||
// Needed for when near completion, or in case of a very short slider.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@@ -13,8 +14,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public abstract partial class FollowCircle : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
protected DrawableHitObject? ParentObject { get; private set; }
|
||||
protected DrawableSlider? DrawableObject { get; private set; }
|
||||
|
||||
private readonly IBindable<bool> tracking = new Bindable<bool>();
|
||||
|
||||
protected FollowCircle()
|
||||
{
|
||||
@@ -22,65 +24,73 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(DrawableHitObject? hitObject)
|
||||
{
|
||||
((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking =>
|
||||
DrawableObject = hitObject as DrawableSlider;
|
||||
|
||||
if (DrawableObject != null)
|
||||
{
|
||||
Debug.Assert(ParentObject != null);
|
||||
|
||||
if (ParentObject.Judged)
|
||||
return;
|
||||
|
||||
using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0)))
|
||||
tracking.BindTo(DrawableObject.Tracking);
|
||||
tracking.BindValueChanged(tracking =>
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
OnSliderPress();
|
||||
else
|
||||
OnSliderRelease();
|
||||
}
|
||||
}, true);
|
||||
if (DrawableObject.Judged)
|
||||
return;
|
||||
|
||||
using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0)))
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
OnSliderPress();
|
||||
else
|
||||
OnSliderRelease();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (ParentObject != null)
|
||||
if (DrawableObject != null)
|
||||
{
|
||||
ParentObject.HitObjectApplied += onHitObjectApplied;
|
||||
onHitObjectApplied(ParentObject);
|
||||
DrawableObject.HitObjectApplied += onHitObjectApplied;
|
||||
onHitObjectApplied(DrawableObject);
|
||||
|
||||
ParentObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(ParentObject, ParentObject.State.Value);
|
||||
DrawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(DrawableObject, DrawableObject.State.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||
{
|
||||
// Sane defaults when a new hitobject is applied to the drawable slider.
|
||||
this.ScaleTo(1f)
|
||||
.FadeOut();
|
||||
|
||||
// Immediately play out any pending transforms from press/release
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
|
||||
private void updateStateTransforms(DrawableHitObject d, ArmedState state)
|
||||
{
|
||||
Debug.Assert(ParentObject != null);
|
||||
Debug.Assert(DrawableObject != null);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
switch (drawableObject)
|
||||
switch (d)
|
||||
{
|
||||
case DrawableSliderTail:
|
||||
// Use ParentObject instead of drawableObject because slider tail's
|
||||
// Use DrawableObject instead of local object because slider tail's
|
||||
// HitStateUpdateTime is ~36ms before the actual slider end (aka slider
|
||||
// tail leniency)
|
||||
using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime))
|
||||
using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime))
|
||||
OnSliderEnd();
|
||||
break;
|
||||
|
||||
case DrawableSliderTick:
|
||||
case DrawableSliderRepeat:
|
||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
|
||||
OnSliderTick();
|
||||
break;
|
||||
}
|
||||
@@ -88,15 +98,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
switch (drawableObject)
|
||||
switch (d)
|
||||
{
|
||||
case DrawableSliderTail:
|
||||
case DrawableSliderTick:
|
||||
case DrawableSliderRepeat:
|
||||
// Despite above comment, ok to use drawableObject.HitStateUpdateTime
|
||||
// Despite above comment, ok to use d.HitStateUpdateTime
|
||||
// here, since on stable, the break anim plays right when the tail is
|
||||
// missed, not when the slider ends
|
||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
|
||||
OnSliderBreak();
|
||||
break;
|
||||
}
|
||||
@@ -109,10 +119,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (ParentObject != null)
|
||||
if (DrawableObject != null)
|
||||
{
|
||||
ParentObject.HitObjectApplied -= onHitObjectApplied;
|
||||
ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
DrawableObject.HitObjectApplied -= onHitObjectApplied;
|
||||
DrawableObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
protected override void OnSliderPress()
|
||||
{
|
||||
Debug.Assert(ParentObject != null);
|
||||
Debug.Assert(DrawableObject != null);
|
||||
|
||||
double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current);
|
||||
double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current);
|
||||
|
||||
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
|
||||
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
}
|
||||
|
||||
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
|
||||
Vector2 pos = new Vector2();
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
combo.Anchor = Anchor.BottomLeft;
|
||||
combo.Origin = Anchor.BottomLeft;
|
||||
combo.Scale = new Vector2(1.28f);
|
||||
|
||||
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
|
||||
}
|
||||
|
||||
if (spectatorList != null)
|
||||
{
|
||||
spectatorList.Anchor = Anchor.BottomLeft;
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
spectatorList.Position = pos;
|
||||
}
|
||||
})
|
||||
{
|
||||
@@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
new LegacyDefaultComboCounter(),
|
||||
new LegacyKeyCounterDisplay(),
|
||||
new SpectatorList(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new osuTK.Vector2(0.5f),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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 osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@@ -16,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
|
||||
public new Hit HitObject => (Hit)base.HitObject;
|
||||
|
||||
[Resolved]
|
||||
private TaikoHitObjectComposer? composer { get; set; }
|
||||
|
||||
public HitPlacementBlueprint()
|
||||
: base(new Hit())
|
||||
{
|
||||
@@ -40,10 +44,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
base.UpdateTimeAndPosition(result);
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
@@ -26,12 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
|
||||
private readonly IHasDuration spanPlacementObject;
|
||||
|
||||
[Resolved]
|
||||
private TaikoHitObjectComposer? composer { get; set; }
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0);
|
||||
|
||||
public TaikoSpanPlacementBlueprint(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
spanPlacementObject = hitObject as IHasDuration;
|
||||
spanPlacementObject = (hitObject as IHasDuration)!;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@@ -79,9 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
EndPlacement(true);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
|
||||
if (PlacementActive == PlacementState.Active)
|
||||
{
|
||||
@@ -116,6 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
originalPosition = ToLocalSpace(result.ScreenSpacePosition);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public partial class TaikoBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public TaikoBlueprintContainer(HitObjectComposer composer)
|
||||
public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer;
|
||||
|
||||
public TaikoBlueprintContainer(TaikoHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
}
|
||||
@@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) =>
|
||||
new TaikoSelectionBlueprint(hitObject);
|
||||
|
||||
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||
{
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = Composer.FindSnappedPositionAndTime(movePosition);
|
||||
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||
if (moved)
|
||||
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||
return moved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer<TaikoHitObject>
|
||||
{
|
||||
protected override bool ApplyHorizontalCentering => false;
|
||||
|
||||
@@ -62,10 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
if (h is not TaikoStrongableHitObject strongable) return;
|
||||
|
||||
if (strongable.IsStrong != state)
|
||||
{
|
||||
strongable.IsStrong = state;
|
||||
EditorBeatmap.Update(strongable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,10 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (h is Hit taikoHit)
|
||||
{
|
||||
taikoHit.Type = state ? HitType.Rim : HitType.Centre;
|
||||
EditorBeatmap.Update(h);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
@@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSwell : DrawableTaikoHitObject<Swell>
|
||||
{
|
||||
private const float target_ring_thick_border = 1.4f;
|
||||
private const float target_ring_thin_border = 1f;
|
||||
private const float target_ring_scale = 5f;
|
||||
private const float inner_ring_alpha = 0.65f;
|
||||
|
||||
/// <summary>
|
||||
/// Offset away from the start time of the swell at which the ring starts appearing.
|
||||
/// </summary>
|
||||
@@ -38,9 +28,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private Vector2 baseSize;
|
||||
|
||||
private readonly Container<DrawableSwellTick> ticks;
|
||||
private readonly Container bodyContainer;
|
||||
private readonly CircularContainer targetRing;
|
||||
private readonly CircularContainer expandingRing;
|
||||
|
||||
private double? lastPressHandleTime;
|
||||
|
||||
@@ -51,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
/// </summary>
|
||||
public bool MustAlternate { get; internal set; } = true;
|
||||
|
||||
public event Action<int> UpdateHitProgress;
|
||||
|
||||
public DrawableSwell()
|
||||
: this(null)
|
||||
{
|
||||
@@ -61,87 +50,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
FillMode = FillMode.Fit;
|
||||
|
||||
Content.Add(bodyContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 1,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
expandingRing = new CircularContainer
|
||||
{
|
||||
Name = "Expanding ring",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = inner_ring_alpha,
|
||||
}
|
||||
}
|
||||
},
|
||||
targetRing = new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thick border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thick_border,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thin border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thin_border,
|
||||
BorderColour = Color4.White,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddInternal(ticks = new Container<DrawableSwellTick> { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
expandingRing.Colour = colours.YellowLight;
|
||||
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
|
||||
}
|
||||
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell),
|
||||
_ => new SwellCirclePiece
|
||||
_ => new DefaultSwell
|
||||
{
|
||||
// to allow for rotation transform
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
protected override void RecreatePieces()
|
||||
@@ -208,16 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
int numHits = ticks.Count(r => r.IsHit);
|
||||
|
||||
float completion = (float)numHits / HitObject.RequiredHits;
|
||||
|
||||
expandingRing
|
||||
.FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50)
|
||||
.Then()
|
||||
.FadeTo(completion / 8, 2000, Easing.OutQuint);
|
||||
|
||||
MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
|
||||
|
||||
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);
|
||||
UpdateHitProgress?.Invoke(numHits);
|
||||
|
||||
if (numHits == HitObject.RequiredHits)
|
||||
ApplyMaxResult();
|
||||
@@ -248,28 +156,21 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms()
|
||||
{
|
||||
base.UpdateStartTimeStateTransforms();
|
||||
|
||||
using (BeginDelayedSequence(-ring_appear_offset))
|
||||
targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
const double transition_duration = 300;
|
||||
base.UpdateHitStateTransforms(state);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
expandingRing.FadeTo(0);
|
||||
break;
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.Delay(300).FadeOut();
|
||||
break;
|
||||
|
||||
case ArmedState.Hit:
|
||||
this.FadeOut(transition_duration, Easing.Out);
|
||||
bodyContainer.ScaleTo(1.4f, transition_duration);
|
||||
this.Delay(660).FadeOut();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
@@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms() => this.FadeOut();
|
||||
|
||||
public void TriggerResult(bool hit)
|
||||
{
|
||||
HitObject.StartTime = Time.Current;
|
||||
@@ -43,7 +39,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick),
|
||||
_ => new TickPiece());
|
||||
protected override SkinnableDrawable CreateMainPiece() => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,9 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (MainPiece != null)
|
||||
Content.Remove(MainPiece, true);
|
||||
|
||||
Content.Add(MainPiece = CreateMainPiece());
|
||||
MainPiece = CreateMainPiece();
|
||||
|
||||
if (MainPiece != null)
|
||||
Content.Add(MainPiece);
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
protected abstract SkinnableDrawable CreateMainPiece();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Game.Rulesets.Taiko.Skinning.Default;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonSwell : DefaultSwell
|
||||
{
|
||||
protected override Drawable CreateCentreCircle()
|
||||
{
|
||||
return new ArgonSwellCirclePiece
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
return new ArgonHitExplosion(taikoComponent.Component);
|
||||
|
||||
case TaikoSkinComponents.Swell:
|
||||
return new ArgonSwellCirclePiece();
|
||||
return new ArgonSwell();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||
{
|
||||
public partial class DefaultSwell : Container
|
||||
{
|
||||
private const float target_ring_thick_border = 1.4f;
|
||||
private const float target_ring_thin_border = 1f;
|
||||
private const float target_ring_scale = 5f;
|
||||
private const float inner_ring_alpha = 0.65f;
|
||||
|
||||
private DrawableSwell drawableSwell = null!;
|
||||
|
||||
private readonly Container bodyContainer;
|
||||
private readonly CircularContainer targetRing;
|
||||
private readonly CircularContainer expandingRing;
|
||||
private readonly Drawable centreCircle;
|
||||
private int numHits;
|
||||
|
||||
public DefaultSwell()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Content.Add(bodyContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 1,
|
||||
Children = new[]
|
||||
{
|
||||
expandingRing = new CircularContainer
|
||||
{
|
||||
Name = "Expanding ring",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = inner_ring_alpha,
|
||||
}
|
||||
}
|
||||
},
|
||||
targetRing = new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thick border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thick_border,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Name = "Target ring (thin border)",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = target_ring_thin_border,
|
||||
BorderColour = Color4.White,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
centreCircle = CreateCentreCircle(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject hitObject, OsuColour colours)
|
||||
{
|
||||
drawableSwell = (DrawableSwell)hitObject;
|
||||
drawableSwell.UpdateHitProgress += animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState += updateStateTransforms;
|
||||
|
||||
expandingRing.Colour = colours.YellowLight;
|
||||
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
|
||||
}
|
||||
|
||||
protected virtual Drawable CreateCentreCircle()
|
||||
{
|
||||
return new SwellCirclePiece
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
|
||||
private void animateSwellProgress(int numHits)
|
||||
{
|
||||
this.numHits = numHits;
|
||||
|
||||
float completion = (float)numHits / drawableSwell.HitObject.RequiredHits;
|
||||
expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
float completion = (float)numHits / drawableSwell.HitObject.RequiredHits;
|
||||
|
||||
centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation,
|
||||
(float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed));
|
||||
expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X,
|
||||
1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed)));
|
||||
expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSwell))
|
||||
return;
|
||||
|
||||
Swell swell = drawableSwell.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(swell.StartTime))
|
||||
{
|
||||
if (state == ArmedState.Idle)
|
||||
expandingRing.FadeTo(0);
|
||||
|
||||
const double ring_appear_offset = 100;
|
||||
|
||||
targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime))
|
||||
{
|
||||
const double transition_duration = 300;
|
||||
|
||||
bodyContainer.FadeOut(transition_duration, Easing.OutQuad);
|
||||
bodyContainer.ScaleTo(1.4f, transition_duration);
|
||||
centreCircle.FadeOut(transition_duration, Easing.OutQuad);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSwell.IsNotNull())
|
||||
{
|
||||
drawableSwell.UpdateHitProgress -= animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Audio;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacySwell : Container
|
||||
{
|
||||
private const float scale_adjust = 768f / 480;
|
||||
private static readonly Vector2 swell_display_position = new Vector2(250f, 100f);
|
||||
|
||||
private DrawableSwell drawableSwell = null!;
|
||||
|
||||
private Container bodyContainer = null!;
|
||||
private Sprite warning = null!;
|
||||
private Sprite spinnerCircle = null!;
|
||||
private Sprite approachCircle = null!;
|
||||
private Sprite clearAnimation = null!;
|
||||
private SkinnableSound clearSample = null!;
|
||||
private LegacySpriteText remainingHitsText = null!;
|
||||
|
||||
private bool samplePlayed;
|
||||
|
||||
private int numHits;
|
||||
|
||||
public LegacySwell()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject hitObject, ISkinSource skin)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
warning = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-warning"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f),
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
bodyContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
spinnerCircle = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-circle"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
approachCircle = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-approachcircle"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1.86f * 0.8f),
|
||||
Alpha = 0.8f,
|
||||
},
|
||||
remainingHitsText = new LegacySpriteText(LegacyFont.Score)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(0f, 130f),
|
||||
Scale = Vector2.One,
|
||||
},
|
||||
}
|
||||
},
|
||||
clearAnimation = new Sprite
|
||||
{
|
||||
Texture = skin.GetTexture("spinner-osu"),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
Y = -40,
|
||||
},
|
||||
},
|
||||
},
|
||||
clearSample = new SkinnableSound(new SampleInfo("spinner-osu")),
|
||||
};
|
||||
|
||||
drawableSwell = (DrawableSwell)hitObject;
|
||||
drawableSwell.UpdateHitProgress += animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState += updateStateTransforms;
|
||||
}
|
||||
|
||||
private void animateSwellProgress(int numHits)
|
||||
{
|
||||
this.numHits = numHits;
|
||||
remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture);
|
||||
spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
int requiredHits = drawableSwell.HitObject.RequiredHits;
|
||||
int remainingHits = requiredHits - numHits;
|
||||
remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously(
|
||||
remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed)));
|
||||
|
||||
spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed));
|
||||
spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously(
|
||||
spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed)));
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSwell))
|
||||
return;
|
||||
|
||||
Swell swell = drawableSwell.HitObject;
|
||||
|
||||
using (BeginAbsoluteSequence(swell.StartTime))
|
||||
{
|
||||
if (state == ArmedState.Idle)
|
||||
{
|
||||
remainingHitsText.Text = $"{swell.RequiredHits}";
|
||||
samplePlayed = false;
|
||||
}
|
||||
|
||||
const double body_transition_duration = 200;
|
||||
|
||||
warning.MoveTo(swell_display_position, body_transition_duration)
|
||||
.ScaleTo(3, body_transition_duration, Easing.Out)
|
||||
.FadeOut(body_transition_duration);
|
||||
|
||||
bodyContainer.FadeIn(body_transition_duration);
|
||||
approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime))
|
||||
{
|
||||
const double clear_transition_duration = 300;
|
||||
const double clear_fade_in = 120;
|
||||
|
||||
bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad);
|
||||
spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad);
|
||||
|
||||
if (state == ArmedState.Hit)
|
||||
{
|
||||
if (!samplePlayed)
|
||||
{
|
||||
clearSample.Play();
|
||||
samplePlayed = true;
|
||||
}
|
||||
|
||||
clearAnimation
|
||||
.MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out)
|
||||
.ScaleTo(0.4f)
|
||||
.ScaleTo(1f, clear_fade_in * 2, Easing.Out)
|
||||
.FadeIn()
|
||||
.Delay(clear_fade_in * 3)
|
||||
.FadeOut(clear_fade_in * 2.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSwell.IsNotNull())
|
||||
{
|
||||
drawableSwell.UpdateHitProgress -= animateSwellProgress;
|
||||
drawableSwell.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
return this.GetAnimation("sliderscorepoint", false, false);
|
||||
|
||||
case TaikoSkinComponents.Swell:
|
||||
// todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601).
|
||||
if (GetTexture("spinner-circle") != null)
|
||||
return new LegacySwell();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.HitTarget:
|
||||
|
||||
@@ -59,11 +59,10 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 350,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.45f,
|
||||
Y = 20,
|
||||
Masking = true,
|
||||
FillMode = FillMode.Fit,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
@@ -19,6 +21,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? osuGame { get; set; }
|
||||
|
||||
public TaikoPlayfieldAdjustmentContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@@ -56,6 +61,18 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
|
||||
|
||||
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
||||
|
||||
// on mobile platforms where the base aspect ratio is wider, the taiko playfield
|
||||
// needs to be scaled down to remain playable.
|
||||
if (RuntimeInfo.IsMobile && osuGame != null)
|
||||
{
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
// this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases.
|
||||
const float magic_scale = 1.25f;
|
||||
Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio);
|
||||
}
|
||||
|
||||
Width = 1 / Scale.X;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
@@ -67,17 +68,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
|
||||
|
||||
assertSnapDistance(100 * multiplier, null, true);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
|
||||
{
|
||||
assertSnapDistance(100, new Slider
|
||||
{
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, false);
|
||||
assertSnapDistance(100 * multiplier);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
@@ -87,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
||||
assertSnapDistance(100 * multiplier, new Slider
|
||||
{
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
@@ -96,7 +87,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
|
||||
|
||||
assertSnapDistance(100f / divisor, null, true);
|
||||
assertSnapDistance(100f / divisor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -114,9 +105,8 @@ namespace osu.Game.Tests.Editing
|
||||
};
|
||||
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject));
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject);
|
||||
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
|
||||
assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject);
|
||||
|
||||
assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject);
|
||||
assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject);
|
||||
@@ -164,39 +154,6 @@ namespace osu.Game.Tests.Editing
|
||||
assertDistanceToDuration(400, 1000);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetSnappedDurationFromDistance()
|
||||
{
|
||||
assertSnappedDuration(0, 0);
|
||||
assertSnappedDuration(50, 1000);
|
||||
assertSnappedDuration(100, 1000);
|
||||
assertSnappedDuration(150, 2000);
|
||||
assertSnappedDuration(200, 2000);
|
||||
assertSnappedDuration(250, 3000);
|
||||
|
||||
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
|
||||
|
||||
assertSnappedDuration(0, 0);
|
||||
assertSnappedDuration(50, 0);
|
||||
assertSnappedDuration(100, 1000);
|
||||
assertSnappedDuration(150, 1000);
|
||||
assertSnappedDuration(200, 1000);
|
||||
assertSnappedDuration(250, 1000);
|
||||
|
||||
AddStep("set beat length = 500", () =>
|
||||
{
|
||||
composer.EditorBeatmap.ControlPointInfo.Clear();
|
||||
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||
});
|
||||
|
||||
assertSnappedDuration(50, 0);
|
||||
assertSnappedDuration(100, 500);
|
||||
assertSnappedDuration(150, 500);
|
||||
assertSnappedDuration(200, 500);
|
||||
assertSnappedDuration(250, 500);
|
||||
assertSnappedDuration(400, 1000);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSnappedDistanceFromDistance()
|
||||
{
|
||||
@@ -289,20 +246,17 @@ namespace osu.Game.Tests.Editing
|
||||
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||
}
|
||||
|
||||
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
||||
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null)
|
||||
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null)
|
||||
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
|
||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
|
||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => 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, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private partial class TestHitObjectComposer : OsuHitObjectComposer
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoomModValidity()
|
||||
{
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
|
||||
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
|
||||
// For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment.
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoomFreeModValidity()
|
||||
{
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
|
||||
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
|
||||
// For now, all rate adjustment mods aren't allowed as free mods in multiplayer.
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
|
||||
}
|
||||
|
||||
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ namespace osu.Game.Tests.NonVisual.Ranking
|
||||
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
|
||||
.ToList();
|
||||
|
||||
// Add some red herrings
|
||||
events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null));
|
||||
events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null));
|
||||
|
||||
HitEventExtensions.UnstableRateCalculationResult result = null;
|
||||
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
@@ -57,6 +61,10 @@ namespace osu.Game.Tests.NonVisual.Ranking
|
||||
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
|
||||
.ToList();
|
||||
|
||||
// Add some red herrings
|
||||
events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null));
|
||||
events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null));
|
||||
|
||||
HitEventExtensions.UnstableRateCalculationResult result = null;
|
||||
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
[TestFixture]
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneMetadataClient : OsuTestScene
|
||||
{
|
||||
private TestMetadataClient client = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = client = new TestMetadataClient();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestWatchingMultipleTimesInvokesServerMethodsOnce()
|
||||
{
|
||||
int countBegin = 0;
|
||||
int countEnd = 0;
|
||||
|
||||
IDisposable token1 = null!;
|
||||
IDisposable token2 = null!;
|
||||
|
||||
AddStep("setup", () =>
|
||||
{
|
||||
client.OnBeginWatchingUserPresence += () => countBegin++;
|
||||
client.OnEndWatchingUserPresence += () => countEnd++;
|
||||
});
|
||||
|
||||
AddStep("begin watching presence (1)", () => token1 = client.BeginWatchingUserPresence());
|
||||
AddAssert("server method invoked once", () => countBegin, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("begin watching presence (2)", () => token2 = client.BeginWatchingUserPresence());
|
||||
AddAssert("server method not invoked a second time", () => countBegin, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("end watching presence (1)", () => token1.Dispose());
|
||||
AddAssert("server method not invoked", () => countEnd, () => Is.EqualTo(0));
|
||||
|
||||
AddStep("end watching presence (2)", () => token2.Dispose());
|
||||
AddAssert("server method invoked once", () => countEnd, () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user