mirror of
https://github.com/ppy/osu.git
synced 2025-02-24 15:13:06 +08:00
Merge branch 'master' into cognition
This commit is contained in:
commit
7f3093ee49
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -133,7 +133,7 @@ jobs:
|
|||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
- name: Install .NET Workloads
|
- name: Install .NET Workloads
|
||||||
run: dotnet workload install maui-ios
|
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build -c Debug osu.iOS
|
run: dotnet build -c Debug osu.iOS
|
||||||
|
33
.github/workflows/diffcalc.yml
vendored
33
.github/workflows/diffcalc.yml
vendored
@ -104,6 +104,25 @@ env:
|
|||||||
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
master-environment:
|
||||||
|
name: Save master environment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
HEAD: ${{ steps.get-head.outputs.HEAD }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout osu
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
sparse-checkout: |
|
||||||
|
README.md
|
||||||
|
|
||||||
|
- name: Get HEAD ref
|
||||||
|
id: get-head
|
||||||
|
run: |
|
||||||
|
ref=$(git log -1 --format='%H')
|
||||||
|
echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
check-permissions:
|
check-permissions:
|
||||||
name: Check permissions
|
name: Check permissions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -121,7 +140,7 @@ jobs:
|
|||||||
|
|
||||||
create-comment:
|
create-comment:
|
||||||
name: Create PR comment
|
name: Create PR comment
|
||||||
needs: check-permissions
|
needs: [ master-environment, check-permissions ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||||
steps:
|
steps:
|
||||||
@ -158,7 +177,7 @@ jobs:
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
name: Setup environment
|
name: Setup environment
|
||||||
needs: directory
|
needs: [ master-environment, directory ]
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
env:
|
env:
|
||||||
VARS_JSON: ${{ toJSON(vars) }}
|
VARS_JSON: ${{ toJSON(vars) }}
|
||||||
@ -182,6 +201,10 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Add master environment
|
||||||
|
run: |
|
||||||
|
sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
|
||||||
- name: Add pull-request environment
|
- name: Add pull-request environment
|
||||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||||
run: |
|
run: |
|
||||||
@ -361,8 +384,7 @@ jobs:
|
|||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
comment_tag: ${{ env.EXECUTION_ID }}
|
comment_tag: ${{ env.EXECUTION_ID }}
|
||||||
mode: upsert
|
mode: recreate
|
||||||
create_if_not_exists: false
|
|
||||||
message: |
|
message: |
|
||||||
Target: ${{ needs.generator.outputs.TARGET }}
|
Target: ${{ needs.generator.outputs.TARGET }}
|
||||||
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
||||||
@ -372,8 +394,7 @@ jobs:
|
|||||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||||
with:
|
with:
|
||||||
comment_tag: ${{ env.EXECUTION_ID }}
|
comment_tag: ${{ env.EXECUTION_ID }}
|
||||||
mode: upsert
|
mode: recreate
|
||||||
create_if_not_exists: false
|
|
||||||
message: |
|
message: |
|
||||||
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.912.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1007.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -66,7 +66,7 @@ namespace osu.Desktop.Updater
|
|||||||
{
|
{
|
||||||
Activated = () =>
|
Activated = () =>
|
||||||
{
|
{
|
||||||
restartToApplyUpdate();
|
Task.Run(restartToApplyUpdate);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -88,7 +88,11 @@ namespace osu.Desktop.Updater
|
|||||||
{
|
{
|
||||||
notification = new UpdateProgressNotification
|
notification = new UpdateProgressNotification
|
||||||
{
|
{
|
||||||
CompletionClickAction = restartToApplyUpdate,
|
CompletionClickAction = () =>
|
||||||
|
{
|
||||||
|
Task.Run(restartToApplyUpdate);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Schedule(() => notificationOverlay.Post(notification));
|
Schedule(() => notificationOverlay.Post(notification));
|
||||||
@ -127,13 +131,10 @@ namespace osu.Desktop.Updater
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool restartToApplyUpdate()
|
private async Task restartToApplyUpdate()
|
||||||
{
|
{
|
||||||
// TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665).
|
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
||||||
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
|
|
||||||
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
|
|
||||||
Schedule(() => game.AttemptExit());
|
Schedule(() => game.AttemptExit());
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" />
|
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Benchmarks
|
||||||
|
{
|
||||||
|
public class BenchmarkGeometryUtils : BenchmarkTest
|
||||||
|
{
|
||||||
|
[Params(100, 1000, 2000, 4000, 8000, 10000)]
|
||||||
|
public int N;
|
||||||
|
|
||||||
|
private Vector2[] points = null!;
|
||||||
|
|
||||||
|
public override void SetUp()
|
||||||
|
{
|
||||||
|
points = new Vector2[N];
|
||||||
|
|
||||||
|
for (int i = 0; i < points.Length; ++i)
|
||||||
|
points[i] = new Vector2(RNG.Next(512), RNG.Next(384));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public void MinimumEnclosingCircle() => GeometryUtils.MinimumEnclosingCircle(points);
|
||||||
|
}
|
||||||
|
}
|
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||||
{
|
{
|
||||||
var result = base.SnapForBlueprint(blueprint);
|
var result = base.SnapForBlueprint(blueprint);
|
||||||
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
|
||||||
|
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
|
||||||
|
|
||||||
protected override void AddHitObject(DrawableHitObject hitObject)
|
protected override void AddHitObject(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
|
||||||
|
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestFruitPlacementPosition()
|
public void TestFruitPlacementPosition()
|
||||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
|
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
|
||||||
|
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||||
|
|
||||||
private void addMoveAndClickSteps(double time, float position, bool end = false)
|
private void addMoveAndClickSteps(double time, float position, bool end = false)
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
@ -31,6 +32,7 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch
|
namespace osu.Game.Rulesets.Catch
|
||||||
{
|
{
|
||||||
@ -223,10 +225,28 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new DifficultySection(),
|
new DifficultySection(),
|
||||||
new ColoursSection(),
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(SetupScreen.SPACING),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ResourcesSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
},
|
||||||
|
new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||||
@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
MaxCombo = beatmap.GetMaxCombo(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
public class BananaShowerCompositionTool : HitObjectCompositionTool
|
public class BananaShowerCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public BananaShowerCompositionTool()
|
public BananaShowerCompositionTool()
|
||||||
: base(nameof(BananaShower))
|
: base(nameof(BananaShower))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
{
|
{
|
||||||
public partial class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
|
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||||
where THitObject : CatchHitObject, new()
|
where THitObject : CatchHitObject, new()
|
||||||
{
|
{
|
||||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Caching;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
@ -172,7 +173,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||||
{
|
{
|
||||||
editablePath.AddVertex(rightMouseDownPosition);
|
editablePath.AddVertex(rightMouseDownPosition);
|
||||||
});
|
})
|
||||||
|
{
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();
|
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();
|
||||||
|
|
||||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||||
{
|
{
|
||||||
new FruitCompositionTool(),
|
new FruitCompositionTool(),
|
||||||
new JuiceStreamCompositionTool(),
|
new JuiceStreamCompositionTool(),
|
||||||
@ -114,6 +114,26 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
if (e.Repeat)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
handleToggleViaKey(e);
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyUp(KeyUpEvent e)
|
||||||
|
{
|
||||||
|
handleToggleViaKey(e);
|
||||||
|
base.OnKeyUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleToggleViaKey(KeyboardEvent key)
|
||||||
|
{
|
||||||
|
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
{
|
||||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
public class FruitCompositionTool : HitObjectCompositionTool
|
public class FruitCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public FruitCompositionTool()
|
public FruitCompositionTool()
|
||||||
: base(nameof(Fruit))
|
: base(nameof(Fruit))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit
|
namespace osu.Game.Rulesets.Catch.Edit
|
||||||
{
|
{
|
||||||
public class JuiceStreamCompositionTool : HitObjectCompositionTool
|
public class JuiceStreamCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public JuiceStreamCompositionTool()
|
public JuiceStreamCompositionTool()
|
||||||
: base(nameof(JuiceStream))
|
: base(nameof(JuiceStream))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -36,23 +38,43 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
StartTimeBindable.BindValueChanged(_ => UpdateComboColour());
|
StartTimeBindable.BindValueChanged(_ => UpdateComboColour());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float startScale;
|
||||||
|
private float endScale;
|
||||||
|
|
||||||
|
private float startAngle;
|
||||||
|
private float endAngle;
|
||||||
|
|
||||||
protected override void UpdateInitialTransforms()
|
protected override void UpdateInitialTransforms()
|
||||||
{
|
{
|
||||||
base.UpdateInitialTransforms();
|
base.UpdateInitialTransforms();
|
||||||
|
|
||||||
|
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||||
const float end_scale = 0.6f;
|
const float end_scale = 0.6f;
|
||||||
const float random_scale_range = 1.6f;
|
const float random_scale_range = 1.6f;
|
||||||
|
|
||||||
ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
|
startScale = end_scale + random_scale_range * RandomSingle(3);
|
||||||
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
|
endScale = end_scale;
|
||||||
|
|
||||||
ScalingContainer.RotateTo(getRandomAngle(1))
|
startAngle = getRandomAngle(1);
|
||||||
.Then()
|
endAngle = getRandomAngle(2);
|
||||||
.RotateTo(getRandomAngle(2), HitObject.TimePreempt);
|
|
||||||
|
|
||||||
float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
|
float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt;
|
||||||
|
|
||||||
|
// Clamp scale and rotation at the point of bananas being caught, else let them freely extrapolate.
|
||||||
|
if (Result.IsHit)
|
||||||
|
preemptProgress = Math.Min(1, preemptProgress);
|
||||||
|
|
||||||
|
ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress));
|
||||||
|
ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress);
|
||||||
|
}
|
||||||
|
|
||||||
public override void PlaySamples()
|
public override void PlaySamples()
|
||||||
{
|
{
|
||||||
base.PlaySamples();
|
base.PlaySamples();
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
@ -28,15 +28,24 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
_ => new DropletPiece());
|
_ => new DropletPiece());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float startRotation;
|
||||||
|
|
||||||
protected override void UpdateInitialTransforms()
|
protected override void UpdateInitialTransforms()
|
||||||
{
|
{
|
||||||
base.UpdateInitialTransforms();
|
base.UpdateInitialTransforms();
|
||||||
|
|
||||||
// roughly matches osu-stable
|
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||||
float startRotation = RandomSingle(1) * 20;
|
startRotation = RandomSingle(1) * 20;
|
||||||
double duration = HitObject.TimePreempt + 2000;
|
}
|
||||||
|
|
||||||
ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
// No clamping for droplets. They should be considered indefinitely spinning regardless of time.
|
||||||
|
// They also never end up on the plate, so they shouldn't stop spinning when caught.
|
||||||
|
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000);
|
||||||
|
ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
@ -32,7 +31,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
{
|
{
|
||||||
base.UpdateInitialTransforms();
|
base.UpdateInitialTransforms();
|
||||||
|
|
||||||
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
|
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||||
|
ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||||
{
|
{
|
||||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||||
var pos = column.ScreenSpacePositionAtTime(time);
|
var pos = column.ScreenSpacePositionAtTime(time);
|
||||||
|
@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
||||||
{
|
{
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject);
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestKeyCountChange()
|
public void TestKeyCountChange()
|
||||||
{
|
{
|
||||||
LabelledSliderBar<float> keyCount = null!;
|
FormSliderBar<float> keyCount = null!;
|
||||||
|
|
||||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<LabelledSliderBar<float>>().First(), () => Is.Not.Null);
|
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<float>>().First(), () => Is.Not.Null);
|
||||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||||
AddStep("change key count to 8", () =>
|
AddStep("change key count to 8", () =>
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -92,5 +93,30 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
|
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
|
||||||
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
|
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOffScreenObjectsRemainSelectedOnColumnChange()
|
||||||
|
{
|
||||||
|
AddStep("create objects", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
AddStep("start drag", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First());
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("end drag", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last());
|
||||||
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
|
||||||
|
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||||
|
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
{
|
{
|
||||||
public abstract partial class ManiaPlacementBlueprint<T> : PlacementBlueprint
|
public abstract partial class ManiaPlacementBlueprint<T> : HitObjectPlacementBlueprint
|
||||||
where T : ManiaHitObject
|
where T : ManiaHitObject
|
||||||
{
|
{
|
||||||
protected new T HitObject => (T)base.HitObject;
|
protected new T HitObject => (T)base.HitObject;
|
||||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit
|
namespace osu.Game.Rulesets.Mania.Edit
|
||||||
{
|
{
|
||||||
public class HoldNoteCompositionTool : HitObjectCompositionTool
|
public class HoldNoteCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public HoldNoteCompositionTool()
|
public HoldNoteCompositionTool()
|
||||||
: base("Hold")
|
: base("Hold")
|
||||||
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
|
|
||||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid();
|
protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid();
|
||||||
|
|
||||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||||
{
|
{
|
||||||
new NoteCompositionTool(),
|
new NoteCompositionTool(),
|
||||||
new HoldNoteCompositionTool()
|
new HoldNoteCompositionTool()
|
||||||
|
@ -104,8 +104,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
int minColumn = int.MaxValue;
|
int minColumn = int.MaxValue;
|
||||||
int maxColumn = int.MinValue;
|
int maxColumn = int.MinValue;
|
||||||
|
|
||||||
|
var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>().ToArray();
|
||||||
|
|
||||||
// find min/max in an initial pass before actually performing the movement.
|
// find min/max in an initial pass before actually performing the movement.
|
||||||
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
|
foreach (var obj in selectedObjects)
|
||||||
{
|
{
|
||||||
if (obj.Column < minColumn)
|
if (obj.Column < minColumn)
|
||||||
minColumn = obj.Column;
|
minColumn = obj.Column;
|
||||||
@ -121,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
((ManiaHitObject)h).Column += columnDelta;
|
((ManiaHitObject)h).Column += columnDelta;
|
||||||
maniaPlayfield.Add(h);
|
maniaPlayfield.Add(h);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
|
||||||
|
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
|
||||||
|
// (check blame for detailed explanation).
|
||||||
|
// thus, ensure that selection is preserved manually.
|
||||||
|
EditorBeatmap.SelectedHitObjects.Clear();
|
||||||
|
EditorBeatmap.SelectedHitObjects.AddRange(selectedObjects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit
|
namespace osu.Game.Rulesets.Mania.Edit
|
||||||
{
|
{
|
||||||
public class NoteCompositionTool : HitObjectCompositionTool
|
public class NoteCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public NoteCompositionTool()
|
public NoteCompositionTool()
|
||||||
: base(nameof(Note))
|
: base(nameof(Note))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
|||||||
{
|
{
|
||||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
|
private FormSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||||
private LabelledSwitchButton specialStyle { get; set; } = null!;
|
private FormCheckBox specialStyle { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Editor? editor { get; set; }
|
private Editor? editor { get; set; }
|
||||||
@ -37,77 +37,81 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
|||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
keyCountSlider = new LabelledSliderBar<float>
|
keyCountSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsCsMania,
|
Caption = BeatmapsetsStrings.ShowStatsCsMania,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = "The number of columns in the beatmap",
|
||||||
Description = "The number of columns in the beatmap",
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
specialStyle = new LabelledSwitchButton
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
specialStyle = new FormCheckBox
|
||||||
{
|
{
|
||||||
Label = "Use special (N+1) style",
|
Caption = "Use special (N+1) style",
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||||
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
|
||||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||||
},
|
},
|
||||||
healthDrainSlider = new LabelledSliderBar<float>
|
healthDrainSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.DrainRateDescription,
|
||||||
Description = EditorSetupStrings.DrainRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overallDifficultySlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
overallDifficultySlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
baseVelocitySlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.BaseVelocity,
|
Caption = EditorSetupStrings.BaseVelocity,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
{
|
{
|
||||||
Default = 1.4,
|
Default = 1.4,
|
||||||
MinValue = 0.4,
|
MinValue = 0.4,
|
||||||
MaxValue = 3.6,
|
MaxValue = 3.6,
|
||||||
Precision = 0.01f,
|
Precision = 0.01f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tickRateSlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
tickRateSlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.TickRate,
|
Caption = EditorSetupStrings.TickRate,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.TickRateDescription,
|
||||||
Description = EditorSetupStrings.TickRateDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
{
|
{
|
||||||
Default = 1,
|
Default = 1,
|
||||||
MinValue = 1,
|
MinValue = 1,
|
||||||
MaxValue = 4,
|
MaxValue = 4,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -419,9 +419,12 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
return new ManiaFilterCriteria();
|
return new ManiaFilterCriteria();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new ManiaDifficultySection(),
|
new ManiaDifficultySection(),
|
||||||
|
new ResourcesSection(),
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
||||||
|
@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
||||||
{
|
{
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
|
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
|
||||||
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
|
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
|
||||||
|
|
||||||
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
|
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(2).TriggerClick());
|
||||||
AddAssert("first object rotated 90deg around selection centre",
|
AddAssert("first object rotated 90deg around selection centre",
|
||||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
|
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
|
||||||
AddAssert("second object rotated 90deg around selection centre",
|
AddAssert("second object rotated 90deg around selection centre",
|
||||||
|
@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
|
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
|
||||||
|
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -22,9 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
{
|
{
|
||||||
public partial class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
|
public partial class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
|
||||||
{
|
{
|
||||||
private Slider slider;
|
private Slider slider = null!;
|
||||||
private DrawableSlider drawableObject;
|
private DrawableSlider drawableObject = null!;
|
||||||
private TestSliderBlueprint blueprint;
|
private TestSliderBlueprint blueprint = null!;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public void Setup() => Schedule(() =>
|
||||||
@ -218,6 +216,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
AddAssert("tail positioned correctly",
|
AddAssert("tail positioned correctly",
|
||||||
() => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
|
() => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
AddAssert("end drag marker positioned correctly",
|
||||||
|
() => Precision.AlmostEquals(blueprint.TailOverlay.EndDragMarker!.ToScreenSpace(blueprint.TailOverlay.EndDragMarker.OriginPosition), drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveMouseToControlPoint(int index)
|
private void moveMouseToControlPoint(int index)
|
||||||
@ -230,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkControlPointSelected(int index, bool selected)
|
private void checkControlPointSelected(int index, bool selected)
|
||||||
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
|
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser!.Pieces[index].IsSelected.Value == selected);
|
||||||
|
|
||||||
private partial class TestSliderBlueprint : SliderSelectionBlueprint
|
private partial class TestSliderBlueprint : SliderSelectionBlueprint
|
||||||
{
|
{
|
||||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||||
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
|
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
|
||||||
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
|
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
|
||||||
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
|
public new PathControlPointVisualiser<Slider>? ControlPointVisualiser => base.ControlPointVisualiser;
|
||||||
|
|
||||||
public TestSliderBlueprint(Slider slider)
|
public TestSliderBlueprint(Slider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
|
@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
{
|
{
|
||||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
|
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
|
||||||
|
|
||||||
protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||||
|
|
||||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||||
[TestCase(0.14102693012101306d, 2, "nan-slider")]
|
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(8.9742952703071666d, 239, "diffcalc-test")]
|
[TestCase(8.9825709931204205d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.743180218215227d, 54, "zero-length-sliders")]
|
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.55071082800473514d, 4, "very-fast-slider")]
|
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||||
|
|
||||||
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
|
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||||
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
|
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||||
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
|
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -10,8 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
{
|
{
|
||||||
public static class RhythmEvaluator
|
public static class RhythmEvaluator
|
||||||
{
|
{
|
||||||
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
|
private const int history_time_max = 5 * 1000; // 5 seconds
|
||||||
private const double rhythm_multiplier = 0.75;
|
private const int history_objects_max = 32;
|
||||||
|
private const double rhythm_overall_multiplier = 0.95;
|
||||||
|
private const double rhythm_ratio_multiplier = 12.0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||||
@ -21,15 +25,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
if (current.BaseObject is Spinner)
|
if (current.BaseObject is Spinner)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
int previousIslandSize = 0;
|
|
||||||
|
|
||||||
double rhythmComplexitySum = 0;
|
double rhythmComplexitySum = 0;
|
||||||
int islandSize = 1;
|
|
||||||
|
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3;
|
||||||
|
|
||||||
|
var island = new Island(deltaDifferenceEpsilon);
|
||||||
|
var previousIsland = new Island(deltaDifferenceEpsilon);
|
||||||
|
|
||||||
|
// we can't use dictionary here because we need to compare island with a tolerance
|
||||||
|
// which is impossible to pass into the hash comparer
|
||||||
|
var islandCounts = new List<(Island Island, int Count)>();
|
||||||
|
|
||||||
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
|
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
|
||||||
|
|
||||||
bool firstDeltaSwitch = false;
|
bool firstDeltaSwitch = false;
|
||||||
|
|
||||||
int historicalNoteCount = Math.Min(current.Index, 32);
|
int historicalNoteCount = Math.Min(current.Index, history_objects_max);
|
||||||
|
|
||||||
int rhythmStart = 0;
|
int rhythmStart = 0;
|
||||||
|
|
||||||
@ -39,74 +50,177 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
|
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
|
||||||
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
|
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
|
||||||
|
|
||||||
|
// we go from the furthest object back to the current one
|
||||||
for (int i = rhythmStart; i > 0; i--)
|
for (int i = rhythmStart; i > 0; i--)
|
||||||
{
|
{
|
||||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
||||||
|
|
||||||
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
// scales note 0 to 1 from history to now
|
||||||
|
double timeDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max;
|
||||||
|
double noteDecay = (double)(historicalNoteCount - i) / historicalNoteCount;
|
||||||
|
|
||||||
currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count.
|
double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
|
||||||
|
|
||||||
double currDelta = currObj.StrainTime;
|
double currDelta = currObj.StrainTime;
|
||||||
double prevDelta = prevObj.StrainTime;
|
double prevDelta = prevObj.StrainTime;
|
||||||
double lastDelta = lastObj.StrainTime;
|
double lastDelta = lastObj.StrainTime;
|
||||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
|
||||||
|
|
||||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3));
|
// calculate how much current delta difference deserves a rhythm bonus
|
||||||
|
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
||||||
|
double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta);
|
||||||
|
double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2));
|
||||||
|
|
||||||
windowPenalty = Math.Min(1, windowPenalty);
|
// reduce ratio bonus if delta difference is too big
|
||||||
|
double fraction = Math.Max(prevDelta / currDelta, currDelta / prevDelta);
|
||||||
|
double fractionMultiplier = Math.Clamp(2.0 - fraction / 8.0, 0.0, 1.0);
|
||||||
|
|
||||||
double effectiveRatio = windowPenalty * currRatio;
|
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
|
||||||
|
|
||||||
|
double effectiveRatio = windowPenalty * currRatio * fractionMultiplier;
|
||||||
|
|
||||||
if (firstDeltaSwitch)
|
if (firstDeltaSwitch)
|
||||||
{
|
{
|
||||||
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
|
||||||
{
|
{
|
||||||
if (islandSize < 7)
|
// island is still progressing
|
||||||
islandSize++; // island is still progressing, count size.
|
island.AddDelta((int)currDelta);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
// bpm change is into slider, this is easy acc window
|
||||||
|
if (currObj.BaseObject is Slider)
|
||||||
effectiveRatio *= 0.125;
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
effectiveRatio *= 0.25;
|
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||||
|
if (prevObj.BaseObject is Slider)
|
||||||
|
effectiveRatio *= 0.3;
|
||||||
|
|
||||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
// repeated island polarity (2 -> 4, 3 -> 5)
|
||||||
effectiveRatio *= 0.25;
|
if (island.IsSimilarPolarity(previousIsland))
|
||||||
|
effectiveRatio *= 0.5;
|
||||||
|
|
||||||
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
// previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||||
effectiveRatio *= 0.50;
|
if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon)
|
||||||
|
|
||||||
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
|
||||||
effectiveRatio *= 0.125;
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
|
// repeated island size (ex: triplet -> triplet)
|
||||||
|
// TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
||||||
|
if (previousIsland.DeltaCount == island.DeltaCount)
|
||||||
|
effectiveRatio *= 0.5;
|
||||||
|
|
||||||
|
var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
|
||||||
|
|
||||||
|
if (islandCount != default)
|
||||||
|
{
|
||||||
|
int countIndex = islandCounts.IndexOf(islandCount);
|
||||||
|
|
||||||
|
// only add island to island counts if they're going one after another
|
||||||
|
if (previousIsland.Equals(island))
|
||||||
|
islandCount.Count++;
|
||||||
|
|
||||||
|
// repeated island (ex: triplet -> triplet)
|
||||||
|
double power = logistic(island.Delta, 2.75, 0.24, 14);
|
||||||
|
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
|
||||||
|
|
||||||
|
islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
islandCounts.Add((island, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// scale down the difficulty if the object is doubletappable
|
||||||
|
double doubletapness = prevObj.GetDoubletapness(currObj);
|
||||||
|
effectiveRatio *= 1 - doubletapness * 0.75;
|
||||||
|
|
||||||
|
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay;
|
||||||
|
|
||||||
startRatio = effectiveRatio;
|
startRatio = effectiveRatio;
|
||||||
|
|
||||||
previousIslandSize = islandSize; // log the last island size.
|
previousIsland = island;
|
||||||
|
|
||||||
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
if (prevDelta + deltaDifferenceEpsilon < currDelta) // we're slowing down, stop counting
|
||||||
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
||||||
|
|
||||||
islandSize = 1;
|
island = new Island((int)currDelta, deltaDifferenceEpsilon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
|
else if (prevDelta > currDelta + deltaDifferenceEpsilon) // we're speeding up
|
||||||
{
|
{
|
||||||
// Begin counting island until we change speed again.
|
// Begin counting island until we change speed again.
|
||||||
firstDeltaSwitch = true;
|
firstDeltaSwitch = true;
|
||||||
|
|
||||||
|
// bpm change is into slider, this is easy acc window
|
||||||
|
if (currObj.BaseObject is Slider)
|
||||||
|
effectiveRatio *= 0.6;
|
||||||
|
|
||||||
|
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
|
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||||
|
if (prevObj.BaseObject is Slider)
|
||||||
|
effectiveRatio *= 0.6;
|
||||||
|
|
||||||
startRatio = effectiveRatio;
|
startRatio = effectiveRatio;
|
||||||
islandSize = 1;
|
|
||||||
|
island = new Island((int)currDelta, deltaDifferenceEpsilon);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastObj = prevObj;
|
lastObj = prevObj;
|
||||||
prevObj = currObj;
|
prevObj = currObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x))));
|
||||||
|
|
||||||
|
private class Island : IEquatable<Island>
|
||||||
|
{
|
||||||
|
private readonly double deltaDifferenceEpsilon;
|
||||||
|
|
||||||
|
public Island(double epsilon)
|
||||||
|
{
|
||||||
|
deltaDifferenceEpsilon = epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Island(int delta, double epsilon)
|
||||||
|
{
|
||||||
|
deltaDifferenceEpsilon = epsilon;
|
||||||
|
Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME);
|
||||||
|
DeltaCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Delta { get; private set; } = int.MaxValue;
|
||||||
|
public int DeltaCount { get; private set; }
|
||||||
|
|
||||||
|
public void AddDelta(int delta)
|
||||||
|
{
|
||||||
|
if (Delta == int.MaxValue)
|
||||||
|
Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME);
|
||||||
|
|
||||||
|
DeltaCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSimilarPolarity(Island other)
|
||||||
|
{
|
||||||
|
// TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
||||||
|
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
||||||
|
return DeltaCount % 2 == other.DeltaCount % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(Island? other)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon &&
|
||||||
|
DeltaCount == other.DeltaCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Delta}x{DeltaCount}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
|
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
|
||||||
private const double min_speed_bonus = 75; // ~200BPM
|
private const double min_speed_bonus = 75; // ~200BPM
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
private const double distance_multiplier = 0.94; // WARNING - DECREASE DISTANCE MULTIPLIER TO AVOID JASHIN BUFF
|
private const double distance_multiplier = 0.94;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Evaluates the difficulty of tapping the current object, based on:
|
/// Evaluates the difficulty of tapping the current object, based on:
|
||||||
@ -31,21 +31,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
// derive strainTime for calculation
|
// derive strainTime for calculation
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||||
var osuNextObj = (OsuDifficultyHitObject?)current.Next(0);
|
|
||||||
|
|
||||||
double strainTime = osuCurrObj.StrainTime;
|
double strainTime = osuCurrObj.StrainTime;
|
||||||
double doubletapness = 1;
|
double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
|
||||||
|
|
||||||
// Nerf doubletappable doubles.
|
|
||||||
if (osuNextObj != null)
|
|
||||||
{
|
|
||||||
double currDeltaTime = Math.Max(1, osuCurrObj.DeltaTime);
|
|
||||||
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
|
||||||
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
|
||||||
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
|
||||||
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / osuCurrObj.HitWindowGreat), 2);
|
|
||||||
doubletapness = Math.Pow(speedRatio, 1 - windowRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap deltatime to the OD 300 hitwindow.
|
// Cap deltatime to the OD 300 hitwindow.
|
||||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||||
@ -68,7 +56,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||||
|
|
||||||
// Base difficulty with all bonuses
|
// Base difficulty with all bonuses
|
||||||
// WARNING - CHANGED TO ADDITIVE TO AVOID AKOLIBED BUFF
|
|
||||||
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||||
|
|
||||||
// Apply penalty if there's doubletappable doubles
|
// Apply penalty if there's doubletappable doubles
|
||||||
|
@ -64,6 +64,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
[JsonProperty("slider_factor")]
|
[JsonProperty("slider_factor")]
|
||||||
public double SliderFactor { get; set; }
|
public double SliderFactor { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("aim_difficult_strain_count")]
|
||||||
|
public double AimDifficultStrainCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("speed_difficult_strain_count")]
|
||||||
|
public double SpeedDifficultStrainCount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -114,6 +120,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
||||||
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
|
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
|
||||||
|
|
||||||
|
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
|
||||||
|
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,8 +137,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||||
|
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
||||||
|
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||||
|
|
||||||
DrainRate = onlineInfo.DrainRate;
|
DrainRate = onlineInfo.DrainRate;
|
||||||
HitCircleCount = onlineInfo.CircleCount;
|
HitCircleCount = onlineInfo.CircleCount;
|
||||||
SliderCount = onlineInfo.SliderCount;
|
SliderCount = onlineInfo.SliderCount;
|
||||||
|
@ -63,6 +63,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
||||||
}
|
}
|
||||||
|
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains();
|
||||||
|
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains();
|
||||||
|
|
||||||
if (mods.Any(m => m is OsuModTouchDevice))
|
if (mods.Any(m => m is OsuModTouchDevice))
|
||||||
{
|
{
|
||||||
@ -96,7 +98,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
|
|
||||||
double drainRate = beatmap.Difficulty.DrainRate;
|
double drainRate = beatmap.Difficulty.DrainRate;
|
||||||
int maxCombo = beatmap.GetMaxCombo();
|
|
||||||
|
|
||||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||||
@ -132,10 +133,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
HiddenDifficulty = hiddenRating,
|
HiddenDifficulty = hiddenRating,
|
||||||
FlashlightDifficulty = flashlightRating,
|
FlashlightDifficulty = flashlightRating,
|
||||||
SliderFactor = sliderFactor,
|
SliderFactor = sliderFactor,
|
||||||
|
AimDifficultStrainCount = aimDifficultyStrainCount,
|
||||||
|
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
||||||
ApproachRate = IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, 1800, 1200, 450),
|
ApproachRate = IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, 1800, 1200, 450),
|
||||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||||
DrainRate = drainRate,
|
DrainRate = drainRate,
|
||||||
MaxCombo = maxCombo,
|
MaxCombo = beatmap.GetMaxCombo(),
|
||||||
HitCircleCount = hitCirclesCount,
|
HitCircleCount = hitCirclesCount,
|
||||||
SliderCount = sliderCount,
|
SliderCount = sliderCount,
|
||||||
SpinnerCount = spinnerCount,
|
SpinnerCount = spinnerCount,
|
||||||
|
@ -14,7 +14,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
public class OsuPerformanceCalculator : PerformanceCalculator
|
public class OsuPerformanceCalculator : PerformanceCalculator
|
||||||
{
|
{
|
||||||
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||||
|
|
||||||
|
private bool usingClassicSliderAccuracy;
|
||||||
|
|
||||||
private double accuracy;
|
private double accuracy;
|
||||||
private int scoreMaxCombo;
|
private int scoreMaxCombo;
|
||||||
@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
var osuAttributes = (OsuDifficultyAttributes)attributes;
|
var osuAttributes = (OsuDifficultyAttributes)attributes;
|
||||||
|
|
||||||
|
usingClassicSliderAccuracy = score.Mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);
|
||||||
|
|
||||||
accuracy = score.Accuracy;
|
accuracy = score.Accuracy;
|
||||||
scoreMaxCombo = score.MaxCombo;
|
scoreMaxCombo = score.MaxCombo;
|
||||||
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||||
@ -133,11 +137,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double lengthBonus = CalculateDefaultLengthBonus(totalHits);
|
double lengthBonus = CalculateDefaultLengthBonus(totalHits);
|
||||||
aimValue *= lengthBonus;
|
aimValue *= lengthBonus;
|
||||||
|
|
||||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
|
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
|
||||||
|
|
||||||
aimValue *= getComboScalingFactor(attributes);
|
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
|
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
|
||||||
@ -174,11 +175,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double lengthBonus = CalculateDefaultLengthBonus(totalHits);
|
double lengthBonus = CalculateDefaultLengthBonus(totalHits);
|
||||||
speedValue *= lengthBonus;
|
speedValue *= lengthBonus;
|
||||||
|
|
||||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
|
||||||
|
|
||||||
speedValue *= getComboScalingFactor(attributes);
|
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is OsuModBlinds))
|
if (score.Mods.Any(m => m is OsuModBlinds))
|
||||||
{
|
{
|
||||||
@ -199,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||||
|
|
||||||
// Scale the speed value with accuracy and OD.
|
// Scale the speed value with accuracy and OD.
|
||||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
|
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
|
||||||
|
|
||||||
// Scale the speed value with # of 50s to punish doubletapping.
|
// Scale the speed value with # of 50s to punish doubletapping.
|
||||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||||
@ -215,6 +213,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
||||||
double betterAccuracyPercentage;
|
double betterAccuracyPercentage;
|
||||||
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
|
||||||
|
if (!usingClassicSliderAccuracy)
|
||||||
|
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||||
|
|
||||||
if (amountHitObjectsWithAccuracy > 0)
|
if (amountHitObjectsWithAccuracy > 0)
|
||||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||||
@ -442,6 +442,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return 1 + result;
|
return 1 + result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||||
|
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||||
|
// to make it more punishing on maps with lower amount of hard sections.
|
||||||
|
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
|
|
||||||
|
@ -22,7 +22,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||||
|
|
||||||
private const int min_delta_time = 25;
|
public const int MIN_DELTA_TIME = 25;
|
||||||
|
|
||||||
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
|
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
|
||||||
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
|
||||||
|
|
||||||
@ -152,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
ClockRate = clockRate;
|
ClockRate = clockRate;
|
||||||
|
|
||||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||||
StrainTime = Math.Max(DeltaTime, min_delta_time);
|
StrainTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
|
||||||
|
|
||||||
if (BaseObject is Slider sliderObject)
|
if (BaseObject is Slider sliderObject)
|
||||||
{
|
{
|
||||||
@ -509,6 +510,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
|
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns how possible is it to doubletap this object together with the next one and get perfect judgement in range from 0 to 1
|
||||||
|
/// </summary>
|
||||||
|
public double GetDoubletapness(OsuDifficultyHitObject? osuNextObj)
|
||||||
|
{
|
||||||
|
if (osuNextObj != null)
|
||||||
|
{
|
||||||
|
double currDeltaTime = Math.Max(1, DeltaTime);
|
||||||
|
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
|
||||||
|
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
|
||||||
|
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
|
||||||
|
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindowGreat), 2);
|
||||||
|
return 1.0 - Math.Pow(speedRatio, 1 - windowRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private void setDistances(double clockRate)
|
private void setDistances(double clockRate)
|
||||||
{
|
{
|
||||||
if (BaseObject is Slider currentSlider)
|
if (BaseObject is Slider currentSlider)
|
||||||
@ -516,7 +535,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
computeSliderCursorPosition(currentSlider);
|
computeSliderCursorPosition(currentSlider);
|
||||||
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
// Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||||
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
|
||||||
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
|
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||||
@ -540,8 +559,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
|
|
||||||
if (lastObject is Slider lastSlider)
|
if (lastObject is Slider lastSlider)
|
||||||
{
|
{
|
||||||
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
|
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
|
||||||
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
|
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, MIN_DELTA_TIME);
|
||||||
|
|
||||||
//
|
//
|
||||||
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
private readonly bool withSliders;
|
private readonly bool withSliders;
|
||||||
|
|
||||||
protected double CurrentStrain;
|
protected double CurrentStrain;
|
||||||
protected double SkillMultiplier => 25.15; // WARNING - INCREASED FROM 24.963 FOR BALANCING
|
protected double SkillMultiplier => 25.18; // WARNING - INCREASED FROM 24.963 FOR BALANCING
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => CurrentStrain * StrainDecay(time - current.Previous(0).StartTime);
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => CurrentStrain * StrainDecay(time - current.Previous(0).StartTime);
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
{
|
{
|
||||||
CurrentStrain *= StrainDecay(current.DeltaTime);
|
CurrentStrain *= StrainDecay(current.DeltaTime);
|
||||||
CurrentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * SkillMultiplier;
|
CurrentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * SkillMultiplier;
|
||||||
|
ObjectStrains.Add(currentStrain);
|
||||||
|
|
||||||
return CurrentStrain;
|
return CurrentStrain;
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
protected virtual double StrainDecayBase => 0.15;
|
protected virtual double StrainDecayBase => 0.15;
|
||||||
|
|
||||||
protected double StrainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
|
protected double StrainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
|
||||||
|
protected List<double> ObjectStrains = new List<double>();
|
||||||
|
protected double Difficulty;
|
||||||
|
|
||||||
protected OsuStrainSkill(Mod[] mods)
|
protected OsuStrainSkill(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
@ -34,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
public override double DifficultyValue()
|
public override double DifficultyValue()
|
||||||
{
|
{
|
||||||
double difficulty = 0;
|
Difficulty = 0;
|
||||||
double weight = 1;
|
double weight = 1;
|
||||||
|
|
||||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
@ -54,11 +56,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
// We're sorting from highest to lowest strain.
|
// We're sorting from highest to lowest strain.
|
||||||
foreach (double strain in strains.OrderDescending())
|
foreach (double strain in strains.OrderDescending())
|
||||||
{
|
{
|
||||||
difficulty += strain * weight;
|
Difficulty += strain * weight;
|
||||||
weight *= DecayWeight;
|
weight *= DecayWeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return difficulty;
|
return Difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of strains weighted against the top strain.
|
||||||
|
/// The result is scaled by clock rate as it affects the total number of strains.
|
||||||
|
/// </summary>
|
||||||
|
public double CountDifficultStrains()
|
||||||
|
{
|
||||||
|
if (Difficulty == 0)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
|
||||||
|
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||||
|
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -6,7 +6,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
@ -24,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
protected override int ReducedSectionCount => 5;
|
protected override int ReducedSectionCount => 5;
|
||||||
|
|
||||||
private readonly List<double> objectStrains = new List<double>();
|
|
||||||
|
|
||||||
public Speed(Mod[] mods)
|
public Speed(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
@ -43,22 +40,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
CurrentRhythm = currODHO.RhythmDifficulty;
|
CurrentRhythm = currODHO.RhythmDifficulty;
|
||||||
double totalStrain = CurrentStrain * CurrentRhythm;
|
double totalStrain = CurrentStrain * CurrentRhythm;
|
||||||
|
|
||||||
objectStrains.Add(totalStrain);
|
ObjectStrains.Add(totalStrain);
|
||||||
|
|
||||||
return totalStrain;
|
return totalStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double RelevantNoteCount()
|
public double RelevantNoteCount()
|
||||||
{
|
{
|
||||||
if (objectStrains.Count == 0)
|
if (ObjectStrains.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double maxStrain = objectStrains.Max();
|
double maxStrain = ObjectStrains.Max();
|
||||||
|
|
||||||
if (maxStrain == 0)
|
if (maxStrain == 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||||
{
|
{
|
||||||
public partial class HitCirclePlacementBlueprint : PlacementBlueprint
|
public partial class HitCirclePlacementBlueprint : HitObjectPlacementBlueprint
|
||||||
{
|
{
|
||||||
public new HitCircle HitObject => (HitCircle)base.HitObject;
|
public new HitCircle HitObject => (HitCircle)base.HitObject;
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (segment.Count == 0)
|
if (segment.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var first = segment[0];
|
PathControlPoint first = segment[0];
|
||||||
|
|
||||||
if (first.Type != PathType.PERFECT_CURVE)
|
if (first.Type != PathType.PERFECT_CURVE)
|
||||||
return;
|
return;
|
||||||
@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (selectedPieces.Length != 1)
|
if (selectedPieces.Length != 1)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var selectedPiece = selectedPieces.Single();
|
PathControlPointPiece<T> selectedPiece = selectedPieces.Single();
|
||||||
var selectedPoint = selectedPiece.ControlPoint;
|
PathControlPoint selectedPoint = selectedPiece.ControlPoint;
|
||||||
|
|
||||||
var validTypes = path_types;
|
PathType?[] validTypes = path_types;
|
||||||
|
|
||||||
if (selectedPoint == controlPoints[0])
|
if (selectedPoint == controlPoints[0])
|
||||||
validTypes = validTypes.Where(t => t != null).ToArray();
|
validTypes = validTypes.Where(t => t != null).ToArray();
|
||||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (Pieces.All(p => !p.IsSelected.Value))
|
if (Pieces.All(p => !p.IsSelected.Value))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var type = path_types[e.Key - Key.Number1];
|
PathType? type = path_types[e.Key - Key.Number1];
|
||||||
|
|
||||||
// The first control point can never be inherit type
|
// The first control point can never be inherit type
|
||||||
if (Pieces[0].IsSelected.Value && type == null)
|
if (Pieces[0].IsSelected.Value && type == null)
|
||||||
@ -353,9 +353,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
{
|
{
|
||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
|
double originalDistance = hitObject.Path.Distance;
|
||||||
|
|
||||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||||
{
|
{
|
||||||
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
List<PathControlPoint> pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||||
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
||||||
|
|
||||||
if (type?.Type == SplineType.PerfectCurve)
|
if (type?.Type == SplineType.PerfectCurve)
|
||||||
@ -375,6 +377,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
EnsureValidPathTypes();
|
EnsureValidPathTypes();
|
||||||
|
|
||||||
|
if (hitObject.Path.Distance < originalDistance)
|
||||||
|
hitObject.SnapTo(distanceSnapProvider);
|
||||||
|
else
|
||||||
|
hitObject.Path.ExpectedDistance.Value = originalDistance;
|
||||||
|
|
||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,14 +412,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public void DragInProgress(DragEvent e)
|
public void DragInProgress(DragEvent e)
|
||||||
{
|
{
|
||||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||||
var oldPosition = hitObject.Position;
|
Vector2 oldPosition = hitObject.Position;
|
||||||
double oldStartTime = hitObject.StartTime;
|
double oldStartTime = hitObject.StartTime;
|
||||||
|
|
||||||
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
|
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
|
||||||
{
|
{
|
||||||
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
// 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]));
|
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||||
|
|
||||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||||
|
|
||||||
@ -421,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||||
{
|
{
|
||||||
var controlPoint = hitObject.Path.ControlPoints[i];
|
PathControlPoint controlPoint = hitObject.Path.ControlPoints[i];
|
||||||
// Since control points are relative to the position of the hit object, all points that are _not_ selected
|
// Since control points are relative to the position of the hit object, all points that are _not_ selected
|
||||||
// need to be offset _back_ by the delta corresponding to the movement of the head point.
|
// need to be offset _back_ by the delta corresponding to the movement of the head point.
|
||||||
// All other selected control points (if any) will move together with the head point
|
// All other selected control points (if any) will move together with the head point
|
||||||
@ -432,13 +439,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||||
|
|
||||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||||
|
|
||||||
for (int i = 0; i < controlPoints.Count; ++i)
|
for (int i = 0; i < controlPoints.Count; ++i)
|
||||||
{
|
{
|
||||||
var controlPoint = controlPoints[i];
|
PathControlPoint controlPoint = controlPoints[i];
|
||||||
if (selectedControlPoints.Contains(controlPoint))
|
if (selectedControlPoints.Contains(controlPoint))
|
||||||
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
||||||
}
|
}
|
||||||
@ -488,8 +495,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
curveTypeItems = new List<MenuItem>();
|
curveTypeItems = new List<MenuItem>();
|
||||||
|
|
||||||
foreach (PathType? type in path_types)
|
for (int i = 0; i < path_types.Length; ++i)
|
||||||
{
|
{
|
||||||
|
PathType? type = path_types[i];
|
||||||
|
|
||||||
// special inherit case
|
// special inherit case
|
||||||
if (type == null)
|
if (type == null)
|
||||||
{
|
{
|
||||||
@ -499,7 +508,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||||
}
|
}
|
||||||
|
|
||||||
curveTypeItems.Add(createMenuItemForPathType(type));
|
curveTypeItems.Add(createMenuItemForPathType(type, InputKey.Number1 + i));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||||
@ -533,7 +542,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
return menuItems.ToArray();
|
return menuItems.ToArray();
|
||||||
|
|
||||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
|
CurveTypeMenuItem createMenuItemForPathType(PathType? type, InputKey? key = null)
|
||||||
|
{
|
||||||
|
Hotkey hotkey = default;
|
||||||
|
|
||||||
|
if (key != null)
|
||||||
|
hotkey = new Hotkey(new KeyCombination(InputKey.Alt, key.Value));
|
||||||
|
|
||||||
|
return new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)) { Hotkey = hotkey };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
if (endDragMarkerContainer != null)
|
if (endDragMarkerContainer != null)
|
||||||
{
|
{
|
||||||
endDragMarkerContainer.Position = circle.Position;
|
endDragMarkerContainer.Position = circle.Position + slider.StackOffset;
|
||||||
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
|
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
|
||||||
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
|
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
|
||||||
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
|
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
|
||||||
|
@ -21,7 +21,7 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||||
{
|
{
|
||||||
public partial class SliderPlacementBlueprint : PlacementBlueprint
|
public partial class SliderPlacementBlueprint : HitObjectPlacementBlueprint
|
||||||
{
|
{
|
||||||
public new Slider HitObject => (Slider)base.HitObject;
|
public new Slider HitObject => (Slider)base.HitObject;
|
||||||
|
|
||||||
@ -401,7 +401,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
if (state == SliderPlacementState.Drawing)
|
if (state == SliderPlacementState.Drawing)
|
||||||
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
||||||
else
|
else
|
||||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
|
||||||
|
|
||||||
bodyPiece.UpdateFrom(HitObject);
|
bodyPiece.UpdateFrom(HitObject);
|
||||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Caching;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -269,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
||||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
|
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
|
||||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,8 +594,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
addControlPoint(lastRightClickPosition);
|
addControlPoint(lastRightClickPosition);
|
||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
}),
|
})
|
||||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
{
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||||
|
},
|
||||||
|
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream)
|
||||||
|
{
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F))
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
|
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
|
||||||
|
@ -13,7 +13,7 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||||
{
|
{
|
||||||
public partial class SpinnerPlacementBlueprint : PlacementBlueprint
|
public partial class SpinnerPlacementBlueprint : HitObjectPlacementBlueprint
|
||||||
{
|
{
|
||||||
public new Spinner HitObject => (Spinner)base.HitObject;
|
public new Spinner HitObject => (Spinner)base.HitObject;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
public class HitCircleCompositionTool : HitObjectCompositionTool
|
public class HitCircleCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public HitCircleCompositionTool()
|
public HitCircleCompositionTool()
|
||||||
: base(nameof(HitCircle))
|
: base(nameof(HitCircle))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -10,10 +11,12 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
@ -90,6 +93,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private ExpandableSlider<float> gridLinesRotationSlider = null!;
|
private ExpandableSlider<float> gridLinesRotationSlider = null!;
|
||||||
private EditorRadioButtonCollection gridTypeButtons = null!;
|
private EditorRadioButtonCollection gridTypeButtons = null!;
|
||||||
|
|
||||||
|
private ExpandableButton useSelectedObjectPositionButton = null!;
|
||||||
|
|
||||||
public OsuGridToolboxGroup()
|
public OsuGridToolboxGroup()
|
||||||
: base("grid")
|
: base("grid")
|
||||||
{
|
{
|
||||||
@ -112,6 +117,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Current = StartPositionY,
|
Current = StartPositionY,
|
||||||
KeyboardStep = 1,
|
KeyboardStep = 1,
|
||||||
},
|
},
|
||||||
|
useSelectedObjectPositionButton = new ExpandableButton
|
||||||
|
{
|
||||||
|
ExpandedLabelText = "Centre on selected object",
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
if (editorBeatmap.SelectedHitObjects.Count != 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var position = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position;
|
||||||
|
StartPosition.Value = new Vector2(MathF.Round(position.X), MathF.Round(position.Y));
|
||||||
|
updateEnabledStates();
|
||||||
|
},
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
},
|
||||||
spacingSlider = new ExpandableSlider<float>
|
spacingSlider = new ExpandableSlider<float>
|
||||||
{
|
{
|
||||||
Current = Spacing,
|
Current = Spacing,
|
||||||
@ -172,6 +191,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
StartPosition.BindValueChanged(pos =>
|
||||||
|
{
|
||||||
|
StartPositionX.Value = pos.NewValue.X;
|
||||||
|
StartPositionY.Value = pos.NewValue.Y;
|
||||||
|
updateEnabledStates();
|
||||||
|
});
|
||||||
|
|
||||||
Spacing.BindValueChanged(spacing =>
|
Spacing.BindValueChanged(spacing =>
|
||||||
{
|
{
|
||||||
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}";
|
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}";
|
||||||
@ -186,12 +212,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
expandingContainer?.Expanded.BindValueChanged(v =>
|
|
||||||
{
|
|
||||||
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
|
||||||
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
GridType.BindValueChanged(v =>
|
GridType.BindValueChanged(v =>
|
||||||
{
|
{
|
||||||
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
||||||
@ -211,6 +231,22 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
editorBeatmap.BeatmapReprocessed += updateEnabledStates;
|
||||||
|
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates());
|
||||||
|
expandingContainer?.Expanded.BindValueChanged(v =>
|
||||||
|
{
|
||||||
|
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||||
|
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||||
|
updateEnabledStates();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateEnabledStates()
|
||||||
|
{
|
||||||
|
useSelectedObjectPositionButton.Enabled.Value = expandingContainer?.Expanded.Value == true
|
||||||
|
&& editorBeatmap.SelectedHitObjects.Count == 1
|
||||||
|
&& !Precision.AlmostEquals(StartPosition.Value, ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position, 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void nextGridSize()
|
private void nextGridSize()
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
|
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
|
||||||
|
|
||||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||||
{
|
{
|
||||||
new HitCircleCompositionTool(),
|
new HitCircleCompositionTool(),
|
||||||
new SliderCompositionTool(),
|
new SliderCompositionTool(),
|
||||||
@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||||
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||||
|
GridToolbox = OsuGridToolboxGroup,
|
||||||
},
|
},
|
||||||
new GenerateToolboxGroup(),
|
new GenerateToolboxGroup(),
|
||||||
FreehandSliderToolboxGroup
|
FreehandSliderToolboxGroup
|
||||||
@ -368,6 +369,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
gridSnapMomentary = shiftPressed;
|
gridSnapMomentary = shiftPressed;
|
||||||
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
@ -25,6 +26,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class OsuSelectionHandler : EditorSelectionHandler
|
public partial class OsuSelectionHandler : EditorSelectionHandler
|
||||||
{
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OsuGridToolboxGroup gridToolbox { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnSelectionChanged()
|
protected override void OnSelectionChanged()
|
||||||
{
|
{
|
||||||
base.OnSelectionChanged();
|
base.OnSelectionChanged();
|
||||||
@ -123,13 +127,43 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
var hitObjects = selectedMovableObjects;
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
// If we're flipping over the origin, we take the grid origin position from the grid toolbox.
|
||||||
|
var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||||
|
Vector2 flipAxis = direction == Direction.Vertical ? Vector2.UnitY : Vector2.UnitX;
|
||||||
|
|
||||||
|
if (flipOverOrigin)
|
||||||
|
{
|
||||||
|
// If we're flipping over the origin, we take one of the axes of the grid.
|
||||||
|
// Take the axis closest to the direction we want to flip over.
|
||||||
|
switch (gridToolbox.GridType.Value)
|
||||||
|
{
|
||||||
|
case PositionSnapGridType.Square:
|
||||||
|
flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 45) % 90 - 45));
|
||||||
|
flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PositionSnapGridType.Triangle:
|
||||||
|
// Hex grid has 3 axes, so you can not directly flip over one of the axes,
|
||||||
|
// however it's still possible to achieve that flip by combining multiple flips over the other axes.
|
||||||
|
// Angle degree range for vertical = (-120, -60]
|
||||||
|
// Angle degree range for horizontal = [-30, 30)
|
||||||
|
flipAxis = direction == Direction.Vertical
|
||||||
|
? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 30) % 60 + 60))
|
||||||
|
: GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlPointFlipQuad = new Quad();
|
||||||
|
|
||||||
bool didFlip = false;
|
bool didFlip = false;
|
||||||
|
|
||||||
foreach (var h in hitObjects)
|
foreach (var h in hitObjects)
|
||||||
{
|
{
|
||||||
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
|
var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position);
|
||||||
|
|
||||||
|
// Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered.
|
||||||
|
flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||||
|
|
||||||
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
||||||
{
|
{
|
||||||
@ -142,12 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
didFlip = true;
|
didFlip = true;
|
||||||
|
|
||||||
foreach (var cp in slider.Path.ControlPoints)
|
foreach (var cp in slider.Path.ControlPoints)
|
||||||
{
|
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
|
||||||
cp.Position = new Vector2(
|
|
||||||
(direction == Direction.Horizontal ? -1 : 1) * cp.Position.X,
|
|
||||||
(direction == Direction.Vertical ? -1 : 1) * cp.Position.Y
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private OsuHitObject[]? objectsInRotation;
|
private OsuHitObject[]? objectsInRotation;
|
||||||
|
|
||||||
private Vector2? defaultOrigin;
|
|
||||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||||
|
|
||||||
@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
objectsInRotation = selectedMovableObjects.ToArray();
|
objectsInRotation = selectedMovableObjects.ToArray();
|
||||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
|
DefaultOrigin = GeometryUtils.MinimumEnclosingCircle(objectsInRotation).Item1;
|
||||||
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
||||||
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
||||||
obj => obj,
|
obj => obj,
|
||||||
@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
if (!OperationInProgress.Value)
|
if (!OperationInProgress.Value)
|
||||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
|
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && DefaultOrigin != null);
|
||||||
|
|
||||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
Vector2 actualOrigin = origin ?? DefaultOrigin.Value;
|
||||||
|
|
||||||
foreach (var ho in objectsInRotation)
|
foreach (var ho in objectsInRotation)
|
||||||
{
|
{
|
||||||
@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
objectsInRotation = null;
|
objectsInRotation = null;
|
||||||
originalPositions = null;
|
originalPositions = null;
|
||||||
originalPathControlPointPositions = null;
|
originalPathControlPointPositions = null;
|
||||||
defaultOrigin = null;
|
DefaultOrigin = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||||
|
@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||||
private Vector2? defaultOrigin;
|
private Vector2? defaultOrigin;
|
||||||
|
private List<Vector2>? originalConvexHull;
|
||||||
|
|
||||||
public override void Begin()
|
public override void Begin()
|
||||||
{
|
{
|
||||||
@ -83,10 +84,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
|
||||||
|
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
|
||||||
|
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
||||||
|
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||||
{
|
{
|
||||||
if (!OperationInProgress.Value)
|
if (!OperationInProgress.Value)
|
||||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||||
@ -94,23 +98,22 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
|
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||||
|
|
||||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||||
|
scale = clampScaleToAdjustAxis(scale, adjustAxis);
|
||||||
|
|
||||||
// for the time being, allow resizing of slider paths only if the slider is
|
// for the time being, allow resizing of slider paths only if the slider is
|
||||||
// the only hit object selected. with a group selection, it's likely the user
|
// the only hit object selected. with a group selection, it's likely the user
|
||||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||||
{
|
{
|
||||||
var originalInfo = objectsInScale[slider];
|
scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation);
|
||||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
|
||||||
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
|
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation);
|
||||||
|
|
||||||
foreach (var (ho, originalState) in objectsInScale)
|
foreach (var (ho, originalState) in objectsInScale)
|
||||||
{
|
{
|
||||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
|
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,21 +137,45 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||||
.Where(h => h is not Spinner);
|
.Where(h => h is not Spinner);
|
||||||
|
|
||||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
|
private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis)
|
||||||
{
|
{
|
||||||
|
switch (adjustAxis)
|
||||||
|
{
|
||||||
|
case Axes.Y:
|
||||||
|
scale.X = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Axes.X:
|
||||||
|
scale.Y = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Axes.None:
|
||||||
|
scale = Vector2.One;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scaleSlider(Slider slider, Vector2 scale, Vector2 origin, OriginalHitObjectState originalInfo, float axisRotation = 0)
|
||||||
|
{
|
||||||
|
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||||
|
|
||||||
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||||
{
|
{
|
||||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
|
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
|
||||||
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snap the slider's length to the current beat divisor
|
// Snap the slider's length to the current beat divisor
|
||||||
// to calculate the final resulting duration / bounding box before the final checks.
|
// to calculate the final resulting duration / bounding box before the final checks.
|
||||||
slider.SnapTo(snapProvider);
|
slider.SnapTo(snapProvider);
|
||||||
|
|
||||||
|
slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
|
||||||
|
|
||||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||||
@ -157,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
|
slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i];
|
||||||
|
|
||||||
|
slider.Position = originalInfo.Position;
|
||||||
|
|
||||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||||
slider.SnapTo(snapProvider);
|
slider.SnapTo(snapProvider);
|
||||||
@ -176,11 +205,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="origin">The origin from which the scale operation is performed</param>
|
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||||
/// <param name="scale">The scale to be clamped</param>
|
/// <param name="scale">The scale to be clamped</param>
|
||||||
|
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||||
|
/// <param name="axisRotation">The rotation of the axes in degrees</param>
|
||||||
/// <returns>The clamped scale vector</returns>
|
/// <returns>The clamped scale vector</returns>
|
||||||
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
|
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||||
{
|
{
|
||||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||||
if (objectsInScale == null)
|
if (objectsInScale == null || adjustAxis == Axes.None)
|
||||||
return scale;
|
return scale;
|
||||||
|
|
||||||
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||||
@ -188,24 +219,60 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||||
origin = slider.Position;
|
origin = slider.Position;
|
||||||
|
|
||||||
|
float cos = MathF.Cos(float.DegreesToRadians(-axisRotation));
|
||||||
|
float sin = MathF.Sin(float.DegreesToRadians(-axisRotation));
|
||||||
|
scale = clampScaleToAdjustAxis(scale, adjustAxis);
|
||||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||||
|
IEnumerable<Vector2> points;
|
||||||
|
|
||||||
|
if (axisRotation == 0)
|
||||||
|
{
|
||||||
var selectionQuad = OriginalSurroundingQuad.Value;
|
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||||
|
points = new[]
|
||||||
|
{
|
||||||
|
selectionQuad.TopLeft,
|
||||||
|
selectionQuad.TopRight,
|
||||||
|
selectionQuad.BottomLeft,
|
||||||
|
selectionQuad.BottomRight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
points = originalConvexHull!;
|
||||||
|
|
||||||
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
foreach (var point in points)
|
||||||
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
{
|
||||||
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
scale = clampToBound(scale, point, Vector2.Zero);
|
||||||
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
||||||
|
}
|
||||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
|
|
||||||
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
|
||||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
|
|
||||||
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
|
||||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
|
|
||||||
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
|
||||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
|
|
||||||
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
|
||||||
|
|
||||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
|
||||||
|
|
||||||
|
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
||||||
|
{
|
||||||
|
p -= actualOrigin;
|
||||||
|
bound -= actualOrigin;
|
||||||
|
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
||||||
|
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||||
|
|
||||||
|
switch (adjustAxis)
|
||||||
|
{
|
||||||
|
case Axes.X:
|
||||||
|
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Axes.Y:
|
||||||
|
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Axes.Both:
|
||||||
|
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveSelectionInBounds()
|
private void moveSelectionInBounds()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -19,16 +20,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
private readonly SelectionRotationHandler rotationHandler;
|
private readonly SelectionRotationHandler rotationHandler;
|
||||||
|
|
||||||
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
|
private readonly OsuGridToolboxGroup gridToolbox;
|
||||||
|
|
||||||
|
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.GridCentre));
|
||||||
|
|
||||||
private SliderWithTextBoxInput<float> angleInput = null!;
|
private SliderWithTextBoxInput<float> angleInput = null!;
|
||||||
private EditorRadioButtonCollection rotationOrigin = null!;
|
private EditorRadioButtonCollection rotationOrigin = null!;
|
||||||
|
|
||||||
private RadioButton selectionCentreButton = null!;
|
private RadioButton selectionCentreButton = null!;
|
||||||
|
|
||||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
|
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
|
||||||
{
|
{
|
||||||
this.rotationHandler = rotationHandler;
|
this.rotationHandler = rotationHandler;
|
||||||
|
this.gridToolbox = gridToolbox;
|
||||||
|
|
||||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
}
|
}
|
||||||
@ -58,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Items = new[]
|
Items = new[]
|
||||||
{
|
{
|
||||||
|
new RadioButton("Grid centre",
|
||||||
|
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre },
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||||
new RadioButton("Playfield centre",
|
new RadioButton("Playfield centre",
|
||||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||||
@ -93,10 +100,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
rotationInfo.BindValueChanged(rotation =>
|
rotationInfo.BindValueChanged(rotation =>
|
||||||
{
|
{
|
||||||
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
|
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
|
||||||
|
rotation.Origin switch
|
||||||
|
{
|
||||||
|
RotationOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||||
|
RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
RotationOrigin.SelectionCentre => null,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
|
||||||
|
};
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
base.PopIn();
|
base.PopIn();
|
||||||
@ -114,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public enum RotationOrigin
|
public enum RotationOrigin
|
||||||
{
|
{
|
||||||
|
GridCentre,
|
||||||
PlayfieldCentre,
|
PlayfieldCentre,
|
||||||
SelectionCentre
|
SelectionCentre
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -20,28 +23,36 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
private readonly OsuSelectionScaleHandler scaleHandler;
|
private readonly OsuSelectionScaleHandler scaleHandler;
|
||||||
|
|
||||||
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
|
private readonly OsuGridToolboxGroup gridToolbox;
|
||||||
|
|
||||||
|
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true));
|
||||||
|
|
||||||
private SliderWithTextBoxInput<float> scaleInput = null!;
|
private SliderWithTextBoxInput<float> scaleInput = null!;
|
||||||
private BindableNumber<float> scaleInputBindable = null!;
|
private BindableNumber<float> scaleInputBindable = null!;
|
||||||
private EditorRadioButtonCollection scaleOrigin = null!;
|
private EditorRadioButtonCollection scaleOrigin = null!;
|
||||||
|
|
||||||
|
private RadioButton gridCentreButton = null!;
|
||||||
private RadioButton playfieldCentreButton = null!;
|
private RadioButton playfieldCentreButton = null!;
|
||||||
private RadioButton selectionCentreButton = null!;
|
private RadioButton selectionCentreButton = null!;
|
||||||
|
|
||||||
private OsuCheckbox xCheckBox = null!;
|
private OsuCheckbox xCheckBox = null!;
|
||||||
private OsuCheckbox yCheckBox = null!;
|
private OsuCheckbox yCheckBox = null!;
|
||||||
|
|
||||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
|
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||||
|
|
||||||
|
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
|
||||||
{
|
{
|
||||||
this.scaleHandler = scaleHandler;
|
this.scaleHandler = scaleHandler;
|
||||||
|
this.gridToolbox = gridToolbox;
|
||||||
|
|
||||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(EditorBeatmap editorBeatmap)
|
||||||
{
|
{
|
||||||
|
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
|
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Width = 220,
|
Width = 220,
|
||||||
@ -66,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Items = new[]
|
Items = new[]
|
||||||
{
|
{
|
||||||
|
gridCentreButton = new RadioButton("Grid centre",
|
||||||
|
() => setOrigin(ScaleOrigin.GridCentre),
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||||
playfieldCentreButton = new RadioButton("Playfield centre",
|
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||||
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||||
@ -97,6 +111,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
gridCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
|
{
|
||||||
|
gridCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to grid centre." : string.Empty;
|
||||||
|
};
|
||||||
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
|
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
{
|
{
|
||||||
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
|
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
|
||||||
@ -123,19 +141,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||||
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||||
|
gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled;
|
||||||
|
|
||||||
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||||
|
|
||||||
scaleInfo.BindValueChanged(scale =>
|
scaleInfo.BindValueChanged(scale =>
|
||||||
{
|
{
|
||||||
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
|
var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale);
|
||||||
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
|
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAxisCheckBoxesEnabled()
|
private void updateAxisCheckBoxesEnabled()
|
||||||
{
|
{
|
||||||
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
|
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
||||||
{
|
{
|
||||||
toggleAxisAvailable(xCheckBox.Current, true);
|
toggleAxisAvailable(xCheckBox.Current, true);
|
||||||
toggleAxisAvailable(yCheckBox.Current, true);
|
toggleAxisAvailable(yCheckBox.Current, true);
|
||||||
@ -162,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
const float max_scale = 10;
|
const float max_scale = 10;
|
||||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
|
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||||
|
|
||||||
if (!scaleInfo.Value.XAxis)
|
if (!scaleInfo.Value.XAxis)
|
||||||
scale.X = max_scale;
|
scale.X = max_scale;
|
||||||
@ -179,7 +198,30 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
updateAxisCheckBoxesEnabled();
|
updateAxisCheckBoxesEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
|
private Vector2? getOriginPosition(PreciseScaleInfo scale)
|
||||||
|
{
|
||||||
|
switch (scale.Origin)
|
||||||
|
{
|
||||||
|
case ScaleOrigin.GridCentre:
|
||||||
|
return gridToolbox.StartPosition.Value;
|
||||||
|
|
||||||
|
case ScaleOrigin.PlayfieldCentre:
|
||||||
|
return OsuPlayfield.BASE_SIZE / 2;
|
||||||
|
|
||||||
|
case ScaleOrigin.SelectionCentre:
|
||||||
|
if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
|
||||||
|
return slider.Position;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(scale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
|
||||||
|
|
||||||
|
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||||
|
|
||||||
private void setAxis(bool x, bool y)
|
private void setAxis(bool x, bool y)
|
||||||
{
|
{
|
||||||
@ -204,6 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public enum ScaleOrigin
|
public enum ScaleOrigin
|
||||||
{
|
{
|
||||||
|
GridCentre,
|
||||||
PlayfieldCentre,
|
PlayfieldCentre,
|
||||||
SelectionCentre
|
SelectionCentre
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
|||||||
{
|
{
|
||||||
public partial class OsuDifficultySection : SetupSection
|
public partial class OsuDifficultySection : SetupSection
|
||||||
{
|
{
|
||||||
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!;
|
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!;
|
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> stackLeniency { get; set; } = null!;
|
private FormSliderBar<float> stackLeniency { get; set; } = null!;
|
||||||
|
|
||||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
@ -31,103 +31,110 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
|||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
circleSizeSlider = new LabelledSliderBar<float>
|
circleSizeSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsCs,
|
Caption = BeatmapsetsStrings.ShowStatsCs,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.CircleSizeDescription,
|
||||||
Description = EditorSetupStrings.CircleSizeDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
healthDrainSlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
healthDrainSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.DrainRateDescription,
|
||||||
Description = EditorSetupStrings.DrainRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
approachRateSlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
approachRateSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAr,
|
Caption = BeatmapsetsStrings.ShowStatsAr,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.ApproachRateDescription,
|
||||||
Description = EditorSetupStrings.ApproachRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overallDifficultySlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
overallDifficultySlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
baseVelocitySlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.BaseVelocity,
|
Caption = EditorSetupStrings.BaseVelocity,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
{
|
{
|
||||||
Default = 1.4,
|
Default = 1.4,
|
||||||
MinValue = 0.4,
|
MinValue = 0.4,
|
||||||
MaxValue = 3.6,
|
MaxValue = 3.6,
|
||||||
Precision = 0.01f,
|
Precision = 0.01f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tickRateSlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
tickRateSlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.TickRate,
|
Caption = EditorSetupStrings.TickRate,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.TickRateDescription,
|
||||||
Description = EditorSetupStrings.TickRateDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
{
|
{
|
||||||
Default = 1,
|
Default = 1,
|
||||||
MinValue = 1,
|
MinValue = 1,
|
||||||
MaxValue = 4,
|
MaxValue = 4,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
stackLeniency = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
stackLeniency = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = "Stack Leniency",
|
Caption = "Stack Leniency",
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||||
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
|
||||||
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
||||||
{
|
{
|
||||||
Default = 0.7f,
|
Default = 0.7f,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 1,
|
MaxValue = 1,
|
||||||
Precision = 0.1f
|
Precision = 0.1f
|
||||||
}
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
public class SliderCompositionTool : HitObjectCompositionTool
|
public class SliderCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public SliderCompositionTool()
|
public SliderCompositionTool()
|
||||||
: base(nameof(Slider))
|
: base(nameof(Slider))
|
||||||
@ -26,6 +26,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
{
|
{
|
||||||
public class SpinnerCompositionTool : HitObjectCompositionTool
|
public class SpinnerCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public SpinnerCompositionTool()
|
public SpinnerCompositionTool()
|
||||||
: base(nameof(Spinner))
|
: base(nameof(Spinner))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||||
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
|
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
|
||||||
|
|
||||||
|
public OsuGridToolboxGroup GridToolbox { get; init; } = null!;
|
||||||
|
|
||||||
public TransformToolboxGroup()
|
public TransformToolboxGroup()
|
||||||
: base("transform")
|
: base("transform")
|
||||||
{
|
{
|
||||||
@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
rotateButton = new EditorToolButton("Rotate",
|
rotateButton = new EditorToolButton("Rotate",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||||
() => new PreciseRotationPopover(RotationHandler)),
|
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
||||||
scaleButton = new EditorToolButton("Scale",
|
scaleButton = new EditorToolButton("Scale",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||||
() => new PreciseScalePopover(ScaleHandler))
|
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -91,19 +91,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||||
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
|
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// but at the end apply the transforms now regardless of whether this is a DHO or not.
|
||||||
|
// the above is just to ensure they don't get overwritten later.
|
||||||
applyDim(piece);
|
applyDim(piece);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void applyDim(Drawable piece)
|
protected override void ClearNestedHitObjects()
|
||||||
|
{
|
||||||
|
base.ClearNestedHitObjects();
|
||||||
|
|
||||||
|
// any dimmable pieces that are DHOs will be pooled separately.
|
||||||
|
// `applyDimToDrawableHitObject` is a closure that implicitly captures `this`,
|
||||||
|
// and because of separate pooling of parent and child objects, there is no guarantee that the pieces will be associated with `this` again on re-use.
|
||||||
|
// therefore, clean up the subscription here to avoid crosstalk.
|
||||||
|
// not doing so can result in the callback attempting to read things from `this` when it is in a completely bogus state (not in use or similar).
|
||||||
|
foreach (var piece in DimmablePieces.OfType<DrawableHitObject>())
|
||||||
|
piece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyDim(Drawable piece)
|
||||||
{
|
{
|
||||||
piece.FadeColour(new Color4(195, 195, 195, 255));
|
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||||
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||||
piece.FadeColour(Color4.White, 100);
|
piece.FadeColour(Color4.White, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
private void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
||||||
}
|
|
||||||
|
|
||||||
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
|
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
|
||||||
|
|
||||||
|
@ -204,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
SpanStartTime = e.SpanStartTime,
|
SpanStartTime = e.SpanStartTime,
|
||||||
StartTime = e.Time,
|
StartTime = e.Time,
|
||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -236,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||||
Position = Position + Path.PositionAt(e.PathProgress),
|
Position = Position + Path.PositionAt(e.PathProgress),
|
||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
|
PathProgress = e.PathProgress,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -248,14 +250,27 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
endPositionCache.Invalidate();
|
endPositionCache.Invalidate();
|
||||||
|
|
||||||
if (HeadCircle != null)
|
foreach (var nested in NestedHitObjects)
|
||||||
HeadCircle.Position = Position;
|
{
|
||||||
|
switch (nested)
|
||||||
|
{
|
||||||
|
case SliderHeadCircle headCircle:
|
||||||
|
headCircle.Position = Position;
|
||||||
|
break;
|
||||||
|
|
||||||
if (TailCircle != null)
|
case SliderTailCircle tailCircle:
|
||||||
TailCircle.Position = EndPosition;
|
tailCircle.Position = EndPosition;
|
||||||
|
break;
|
||||||
|
|
||||||
if (LastRepeat != null)
|
case SliderRepeat repeat:
|
||||||
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
|
repeat.Position = Position + Path.PositionAt(repeat.PathProgress);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SliderTick tick:
|
||||||
|
tick.Position = Position + Path.PositionAt(tick.PathProgress);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void UpdateNestedSamples()
|
protected void UpdateNestedSamples()
|
||||||
|
@ -5,6 +5,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
public class SliderRepeat : SliderEndCircle
|
public class SliderRepeat : SliderEndCircle
|
||||||
{
|
{
|
||||||
|
public double PathProgress { get; set; }
|
||||||
|
|
||||||
public SliderRepeat(Slider slider)
|
public SliderRepeat(Slider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
public int SpanIndex { get; set; }
|
public int SpanIndex { get; set; }
|
||||||
public double SpanStartTime { get; set; }
|
public double SpanStartTime { get; set; }
|
||||||
|
public double PathProgress { get; set; }
|
||||||
|
|
||||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||||
{
|
{
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
@ -39,6 +40,7 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu
|
namespace osu.Game.Rulesets.Osu
|
||||||
{
|
{
|
||||||
@ -336,10 +338,28 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new OsuDifficultySection(),
|
new OsuDifficultySection(),
|
||||||
new ColoursSection(),
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(SetupScreen.SPACING),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ResourcesSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
},
|
||||||
|
new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
||||||
|
@ -232,8 +232,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
||||||
workingObject.EndPositionModified = slider.EndPosition;
|
workingObject.EndPositionModified = slider.EndPosition;
|
||||||
|
|
||||||
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
|
|
||||||
|
|
||||||
return workingObject.PositionModified - previousPosition;
|
return workingObject.PositionModified - previousPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,22 +305,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
return new RectangleF(left, top, right - left, bottom - top);
|
return new RectangleF(left, top, right - left, bottom - top);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
|
|
||||||
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
|
|
||||||
private static void shiftNestedObjects(Slider slider, Vector2 shift)
|
|
||||||
{
|
|
||||||
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
|
|
||||||
{
|
|
||||||
if (!(hitObject is OsuHitObject osuHitObject))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
osuHitObject.Position += shift;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a position to playfield, keeping a specified distance from the edges.
|
/// Clamp a position to playfield, keeping a specified distance from the edges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -431,7 +413,6 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
private class WorkingObject
|
private class WorkingObject
|
||||||
{
|
{
|
||||||
public float RotationOriginal { get; }
|
public float RotationOriginal { get; }
|
||||||
public Vector2 PositionOriginal { get; }
|
|
||||||
public Vector2 PositionModified { get; set; }
|
public Vector2 PositionModified { get; set; }
|
||||||
public Vector2 EndPositionModified { get; set; }
|
public Vector2 EndPositionModified { get; set; }
|
||||||
|
|
||||||
@ -442,7 +423,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
{
|
{
|
||||||
PositionInfo = positionInfo;
|
PositionInfo = positionInfo;
|
||||||
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
|
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
|
||||||
PositionModified = PositionOriginal = HitObject.Position;
|
PositionModified = HitObject.Position;
|
||||||
EndPositionModified = HitObject.EndPosition;
|
EndPositionModified = HitObject.EndPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Taiko.Skinning.Argon;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class VolumeAwareHitSampleInfoTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestVolumeAwareHitSampleInfoIsNotEqualToItsUnderlyingSample(
|
||||||
|
[Values(HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP)]
|
||||||
|
string sample,
|
||||||
|
[Values(HitSampleInfo.BANK_NORMAL, HitSampleInfo.BANK_SOFT)]
|
||||||
|
string bank,
|
||||||
|
[Values(30, 70, 100)] int volume)
|
||||||
|
{
|
||||||
|
var underlyingSample = new HitSampleInfo(sample, bank, volume: volume);
|
||||||
|
var volumeAwareSample = new VolumeAwareHitSampleInfo(underlyingSample);
|
||||||
|
|
||||||
|
Assert.That(underlyingSample, Is.Not.EqualTo(volumeAwareSample));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
[JsonProperty("great_hit_window")]
|
[JsonProperty("great_hit_window")]
|
||||||
public double GreatHitWindow { get; set; }
|
public double GreatHitWindow { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("ok_hit_window")]
|
||||||
|
public double OkHitWindow { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||||
{
|
{
|
||||||
foreach (var v in base.ToDatabaseAttributes())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
@ -50,6 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
|
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@ -58,6 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
|
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
|||||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||||
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Taiko.Mods;
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
|
||||||
using osu.Game.Rulesets.Taiko.Scoring;
|
using osu.Game.Rulesets.Taiko.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
@ -100,7 +99,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
ColourDifficulty = colourRating,
|
ColourDifficulty = colourRating,
|
||||||
PeakDifficulty = combinedRating,
|
PeakDifficulty = combinedRating,
|
||||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
||||||
|
MaxCombo = beatmap.GetMaxCombo(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return attributes;
|
return attributes;
|
||||||
|
@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
[JsonProperty("effective_miss_count")]
|
[JsonProperty("effective_miss_count")]
|
||||||
public double EffectiveMissCount { get; set; }
|
public double EffectiveMissCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("estimated_unstable_rate")]
|
||||||
|
public double? EstimatedUnstableRate { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||||
{
|
{
|
||||||
foreach (var attribute in base.GetAttributesForDisplay())
|
foreach (var attribute in base.GetAttributesForDisplay())
|
||||||
|
@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
{
|
{
|
||||||
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
private int countOk;
|
private int countOk;
|
||||||
private int countMeh;
|
private int countMeh;
|
||||||
private int countMiss;
|
private int countMiss;
|
||||||
private double accuracy;
|
private double? estimatedUnstableRate;
|
||||||
|
|
||||||
private double effectiveMissCount;
|
private double effectiveMissCount;
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
accuracy = customAccuracy;
|
estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;
|
||||||
|
|
||||||
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
|
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
|
||||||
if (totalSuccessfulHits > 0)
|
if (totalSuccessfulHits > 0)
|
||||||
@ -65,6 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
Difficulty = difficultyValue,
|
Difficulty = difficultyValue,
|
||||||
Accuracy = accuracyValue,
|
Accuracy = accuracyValue,
|
||||||
EffectiveMissCount = effectiveMissCount,
|
EffectiveMissCount = effectiveMissCount,
|
||||||
|
EstimatedUnstableRate = estimatedUnstableRate,
|
||||||
Total = totalValue
|
Total = totalValue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -85,35 +87,94 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
difficultyValue *= 1.025;
|
difficultyValue *= 1.025;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModHardRock))
|
if (score.Mods.Any(m => m is ModHardRock))
|
||||||
difficultyValue *= 1.050;
|
difficultyValue *= 1.10;
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||||
difficultyValue *= 1.050 * lengthBonus;
|
difficultyValue *= 1.050 * lengthBonus;
|
||||||
|
|
||||||
return difficultyValue * Math.Pow(accuracy, 2.0);
|
if (estimatedUnstableRate == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUnstableRate.Value)), 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||||
{
|
{
|
||||||
if (attributes.GreatHitWindow <= 0)
|
if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
|
double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;
|
||||||
|
|
||||||
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
||||||
accuracyValue *= lengthBonus;
|
|
||||||
|
|
||||||
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
|
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
|
||||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert)
|
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert)
|
||||||
accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus);
|
accuracyValue *= Math.Max(1.0, 1.05 * lengthBonus);
|
||||||
|
|
||||||
return accuracyValue;
|
return accuracyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes an upper bound on the player's tap deviation based on the OD, number of circles and sliders,
|
||||||
|
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
|
||||||
|
/// two SS scores on the same map with the same settings will always return the same deviation.
|
||||||
|
/// </summary>
|
||||||
|
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
double h300 = attributes.GreatHitWindow;
|
||||||
|
double h100 = attributes.OkHitWindow;
|
||||||
|
|
||||||
|
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||||
|
|
||||||
|
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
|
||||||
|
double? calcDeviationGreatWindow()
|
||||||
|
{
|
||||||
|
if (countGreat == 0) return null;
|
||||||
|
|
||||||
|
double n = totalHits;
|
||||||
|
|
||||||
|
// Proportion of greats hit.
|
||||||
|
double p = countGreat / n;
|
||||||
|
|
||||||
|
// We can be 99% confident that p is at least this value.
|
||||||
|
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||||
|
|
||||||
|
// We can be 99% confident that the deviation is not higher than:
|
||||||
|
return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
|
||||||
|
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
|
||||||
|
double? calcDeviationGoodWindow()
|
||||||
|
{
|
||||||
|
if (totalSuccessfulHits == 0) return null;
|
||||||
|
|
||||||
|
double n = totalHits;
|
||||||
|
|
||||||
|
// Proportion of greats + goods hit.
|
||||||
|
double p = totalSuccessfulHits / n;
|
||||||
|
|
||||||
|
// We can be 99% confident that p is at least this value.
|
||||||
|
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||||
|
|
||||||
|
// We can be 99% confident that the deviation is not higher than:
|
||||||
|
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||||
|
}
|
||||||
|
|
||||||
|
double? deviationGreatWindow = calcDeviationGreatWindow();
|
||||||
|
double? deviationGoodWindow = calcDeviationGoodWindow();
|
||||||
|
|
||||||
|
if (deviationGreatWindow is null)
|
||||||
|
return deviationGoodWindow;
|
||||||
|
|
||||||
|
return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
|
|
||||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||||
|
|
||||||
private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||||
{
|
{
|
||||||
public partial class HitPlacementBlueprint : PlacementBlueprint
|
public partial class HitPlacementBlueprint : HitObjectPlacementBlueprint
|
||||||
{
|
{
|
||||||
private readonly HitPiece piece;
|
private readonly HitPiece piece;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ using osuTK.Input;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||||
{
|
{
|
||||||
public partial class TaikoSpanPlacementBlueprint : PlacementBlueprint
|
public partial class TaikoSpanPlacementBlueprint : HitObjectPlacementBlueprint
|
||||||
{
|
{
|
||||||
private readonly HitPiece headPiece;
|
private readonly HitPiece headPiece;
|
||||||
private readonly HitPiece tailPiece;
|
private readonly HitPiece tailPiece;
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Edit
|
namespace osu.Game.Rulesets.Taiko.Edit
|
||||||
{
|
{
|
||||||
public class DrumRollCompositionTool : HitObjectCompositionTool
|
public class DrumRollCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public DrumRollCompositionTool()
|
public DrumRollCompositionTool()
|
||||||
: base(nameof(DrumRoll))
|
: base(nameof(DrumRoll))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Edit
|
namespace osu.Game.Rulesets.Taiko.Edit
|
||||||
{
|
{
|
||||||
public class HitCompositionTool : HitObjectCompositionTool
|
public class HitCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public HitCompositionTool()
|
public HitCompositionTool()
|
||||||
: base(nameof(Hit))
|
: base(nameof(Hit))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
|||||||
{
|
{
|
||||||
public partial class TaikoDifficultySection : SetupSection
|
public partial class TaikoDifficultySection : SetupSection
|
||||||
{
|
{
|
||||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
|
|
||||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
@ -28,64 +28,68 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
|||||||
{
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
healthDrainSlider = new LabelledSliderBar<float>
|
healthDrainSlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.DrainRateDescription,
|
||||||
Description = EditorSetupStrings.DrainRateDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
overallDifficultySlider = new LabelledSliderBar<float>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
overallDifficultySlider = new FormSliderBar<float>
|
||||||
{
|
{
|
||||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
Caption = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.OverallDifficultyDescription,
|
||||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
|
||||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||||
{
|
{
|
||||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 10,
|
MaxValue = 10,
|
||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
baseVelocitySlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.BaseVelocity,
|
Caption = EditorSetupStrings.BaseVelocity,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
{
|
{
|
||||||
Default = 1.4,
|
Default = 1.4,
|
||||||
MinValue = 0.4,
|
MinValue = 0.4,
|
||||||
MaxValue = 3.6,
|
MaxValue = 3.6,
|
||||||
Precision = 0.01f,
|
Precision = 0.01f,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
tickRateSlider = new LabelledSliderBar<double>
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
tickRateSlider = new FormSliderBar<double>
|
||||||
{
|
{
|
||||||
Label = EditorSetupStrings.TickRate,
|
Caption = EditorSetupStrings.TickRate,
|
||||||
FixedLabelWidth = LABEL_WIDTH,
|
HintText = EditorSetupStrings.TickRateDescription,
|
||||||
Description = EditorSetupStrings.TickRateDescription,
|
|
||||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
{
|
{
|
||||||
Default = 1,
|
Default = 1,
|
||||||
MinValue = 1,
|
MinValue = 1,
|
||||||
MaxValue = 4,
|
MaxValue = 4,
|
||||||
Precision = 1,
|
Precision = 1,
|
||||||
}
|
},
|
||||||
|
TransferValueOnCommit = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
|
|
||||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||||
item.Current.ValueChanged += _ => updateValues();
|
item.Current.ValueChanged += _ => updateValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Edit
|
namespace osu.Game.Rulesets.Taiko.Edit
|
||||||
{
|
{
|
||||||
public class SwellCompositionTool : HitObjectCompositionTool
|
public class SwellCompositionTool : CompositionTool
|
||||||
{
|
{
|
||||||
public SwellCompositionTool()
|
public SwellCompositionTool()
|
||||||
: base(nameof(Swell))
|
: base(nameof(Swell))
|
||||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||||
|
|
||||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
|
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||||
{
|
{
|
||||||
new HitCompositionTool(),
|
new HitCompositionTool(),
|
||||||
new DrumRollCompositionTool(),
|
new DrumRollCompositionTool(),
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -86,10 +87,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||||
{
|
{
|
||||||
if (selection.All(s => s.Item is Hit))
|
if (selection.All(s => s.Item is Hit))
|
||||||
yield return new TernaryStateToggleMenuItem("Rim") { State = { BindTarget = selectionRimState } };
|
{
|
||||||
|
yield return new TernaryStateToggleMenuItem("Rim")
|
||||||
|
{
|
||||||
|
State = { BindTarget = selectionRimState },
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.W), new KeyCombination(InputKey.R)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (selection.All(s => s.Item is TaikoHitObject))
|
if (selection.All(s => s.Item is TaikoHitObject))
|
||||||
yield return new TernaryStateToggleMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
{
|
||||||
|
yield return new TernaryStateToggleMenuItem("Strong")
|
||||||
|
{
|
||||||
|
State = { BindTarget = selectionStrongState },
|
||||||
|
Hotkey = new Hotkey(new KeyCombination(InputKey.E)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||||
yield return item;
|
yield return item;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
|
||||||
@ -48,5 +49,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
|||||||
return originalBank;
|
return originalBank;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool Equals(HitSampleInfo? other) => other is VolumeAwareHitSampleInfo && base.Equals(other);
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// This override attempts to match the <see cref="Equals"/> override above, but in theory it is not strictly necessary.
|
||||||
|
/// Recall that <see cref="GetHashCode"/> <a href="https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-8.0#notes-to-inheritors">must meet the following requirements</a>:
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// "If two objects compare as equal, the <see cref="GetHashCode"/> method for each object must return the same value.
|
||||||
|
/// However, if two objects do not compare as equal, <see cref="GetHashCode"/> methods for the two objects do not have to return different values."
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Making this override combine the value generated by the base <see cref="GetHashCode"/> implementation with a constant means
|
||||||
|
/// that <see cref="HitSampleInfo"/> and <see cref="VolumeAwareHitSampleInfo"/> instances which have the same values of their members
|
||||||
|
/// will not have equal hash codes, which is slightly more efficient when these objects are used as dictionary keys.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,9 +190,12 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
|
||||||
|
|
||||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||||
[
|
[
|
||||||
|
new MetadataSection(),
|
||||||
new TaikoDifficultySection(),
|
new TaikoDifficultySection(),
|
||||||
|
new ResourcesSection(),
|
||||||
|
new DesignSection(),
|
||||||
];
|
];
|
||||||
|
|
||||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
|
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
|
||||||
|
@ -112,6 +112,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
{
|
{
|
||||||
SliderVelocityMultiplier = slider_velocity
|
SliderVelocityMultiplier = slider_velocity
|
||||||
};
|
};
|
||||||
|
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject));
|
||||||
|
|
||||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||||
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
|
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
|
||||||
@ -227,26 +228,65 @@ namespace osu.Game.Tests.Editing
|
|||||||
assertSnappedDistance(400, 400);
|
assertSnappedDistance(400, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUnsnappedObject()
|
||||||
|
{
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
// simulate object snapped to 1/3rds
|
||||||
|
// this object's end time will be 2000 / 3 = 666.66... ms
|
||||||
|
new PathControlPoint(new Vector2(200 / 3f, 0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add slider", () => composer.EditorBeatmap.Add(slider));
|
||||||
|
AddStep("set snap to 1/4", () => BeatDivisor.Value = 4);
|
||||||
|
|
||||||
|
// with default beat length of 1000ms and snap at 1/4, the valid snap times are 500ms, 750ms, and 1000ms
|
||||||
|
// with default settings, the snapped distance will be a tenth of the difference of the time delta
|
||||||
|
|
||||||
|
// (500 - 666.66...) / 10 = -16.66... = -100 / 6
|
||||||
|
assertSnappedDistance(0, -100 / 6f, slider);
|
||||||
|
assertSnappedDistance(7, -100 / 6f, slider);
|
||||||
|
|
||||||
|
// (750 - 666.66...) / 10 = 8.33... = 100 / 12
|
||||||
|
assertSnappedDistance(9, 100 / 12f, slider);
|
||||||
|
assertSnappedDistance(33, 100 / 12f, slider);
|
||||||
|
|
||||||
|
// (1000 - 666.66...) / 10 = 33.33... = 100 / 3
|
||||||
|
assertSnappedDistance(34, 100 / 3f, slider);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestUseCurrentSnap()
|
public void TestUseCurrentSnap()
|
||||||
{
|
{
|
||||||
|
ExpandableButton getCurrentSnapButton() => composer.ChildrenOfType<EditorToolboxGroup>().Single(g => g.Name == "snapping")
|
||||||
|
.ChildrenOfType<ExpandableButton>().Single();
|
||||||
|
|
||||||
AddStep("add objects to beatmap", () =>
|
AddStep("add objects to beatmap", () =>
|
||||||
{
|
{
|
||||||
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
||||||
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
|
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(getCurrentSnapButton()));
|
||||||
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
|
AddUntilStep("use current snap expanded", () => getCurrentSnapButton().Expanded.Value, () => Is.True);
|
||||||
|
|
||||||
AddStep("seek before first object", () => EditorClock.Seek(0));
|
AddStep("seek before first object", () => EditorClock.Seek(0));
|
||||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||||
|
|
||||||
AddStep("seek to between objects", () => EditorClock.Seek(1500));
|
AddStep("seek to between objects", () => EditorClock.Seek(1500));
|
||||||
AddUntilStep("use current snap available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.True);
|
AddUntilStep("use current snap available", () => getCurrentSnapButton().Enabled.Value, () => Is.True);
|
||||||
|
|
||||||
AddStep("seek after last object", () => EditorClock.Seek(2500));
|
AddStep("seek after last object", () => EditorClock.Seek(2500));
|
||||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
||||||
@ -262,7 +302,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
|
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
|
||||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
private partial class TestHitObjectComposer : OsuHitObjectComposer
|
private partial class TestHitObjectComposer : OsuHitObjectComposer
|
||||||
{
|
{
|
||||||
|
52
osu.Game.Tests/Utils/BindableValueAccessorTest.cs
Normal file
52
osu.Game.Tests/Utils/BindableValueAccessorTest.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Utils
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BindableValueAccessorTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void GetValue()
|
||||||
|
{
|
||||||
|
const int value = 1337;
|
||||||
|
|
||||||
|
BindableInt bindable = new BindableInt(value);
|
||||||
|
Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetValue()
|
||||||
|
{
|
||||||
|
const int value = 1337;
|
||||||
|
|
||||||
|
BindableInt bindable = new BindableInt();
|
||||||
|
BindableValueAccessor.SetValue(bindable, value);
|
||||||
|
|
||||||
|
Assert.That(bindable.Value, Is.EqualTo(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetInvalidBindable()
|
||||||
|
{
|
||||||
|
BindableList<object> list = new BindableList<object>();
|
||||||
|
Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SetInvalidBindable()
|
||||||
|
{
|
||||||
|
const int value = 1337;
|
||||||
|
|
||||||
|
BindableList<int> list = new BindableList<int> { value };
|
||||||
|
BindableValueAccessor.SetValue(list, 2);
|
||||||
|
|
||||||
|
Assert.That(list, Has.Exactly(1).Items);
|
||||||
|
Assert.That(list[0], Is.EqualTo(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
osu.Game.Tests/Utils/GeometryUtilsTest.cs
Normal file
51
osu.Game.Tests/Utils/GeometryUtilsTest.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Utils
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class GeometryUtilsTest
|
||||||
|
{
|
||||||
|
[TestCase(new int[] { }, new int[] { })]
|
||||||
|
[TestCase(new[] { 0, 0 }, new[] { 0, 0 })]
|
||||||
|
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
|
||||||
|
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
|
||||||
|
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, new[] { 0, 0, 4, 10, 2, -1 })]
|
||||||
|
public void TestConvexHull(int[] values, int[] expected)
|
||||||
|
{
|
||||||
|
var points = new Vector2[values.Length / 2];
|
||||||
|
for (int i = 0; i < values.Length; i += 2)
|
||||||
|
points[i / 2] = new Vector2(values[i], values[i + 1]);
|
||||||
|
|
||||||
|
var expectedPoints = new Vector2[expected.Length / 2];
|
||||||
|
for (int i = 0; i < expected.Length; i += 2)
|
||||||
|
expectedPoints[i / 2] = new Vector2(expected[i], expected[i + 1]);
|
||||||
|
|
||||||
|
var hull = GeometryUtils.GetConvexHull(points);
|
||||||
|
|
||||||
|
Assert.That(hull, Is.EquivalentTo(expectedPoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(new int[] { }, 0, 0, 0)]
|
||||||
|
[TestCase(new[] { 0, 0 }, 0, 0, 0)]
|
||||||
|
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, 1, 0, 1)]
|
||||||
|
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, 1, 0, 1)]
|
||||||
|
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, 3, 4.5f, 5.5901699f)]
|
||||||
|
public void TestMinimumEnclosingCircle(int[] values, float x, float y, float r)
|
||||||
|
{
|
||||||
|
var points = new Vector2[values.Length / 2];
|
||||||
|
for (int i = 0; i < values.Length; i += 2)
|
||||||
|
points[i / 2] = new Vector2(values[i], values[i + 1]);
|
||||||
|
|
||||||
|
(var centre, float radius) = GeometryUtils.MinimumEnclosingCircle(points);
|
||||||
|
|
||||||
|
Assert.That(centre.X, Is.EqualTo(x).Within(0.0001));
|
||||||
|
Assert.That(centre.Y, Is.EqualTo(y).Within(0.0001));
|
||||||
|
Assert.That(radius, Is.EqualTo(r).Within(0.0001));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal file
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
public partial class TestSceneColoursSection : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestNoBeatmapSkinColours()
|
||||||
|
{
|
||||||
|
LegacyBeatmapSkin skin = null!;
|
||||||
|
ColoursSection coloursSection = null!;
|
||||||
|
|
||||||
|
AddStep("create beatmap skin", () => skin = new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||||
|
AddStep("create colours section", () => Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
|
||||||
|
}, skin)),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
|
||||||
|
],
|
||||||
|
Child = coloursSection = new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AddAssert("beatmap skin has no colours", () => skin.Configuration.CustomComboColours, () => Is.Empty);
|
||||||
|
AddAssert("section displays default combo colours",
|
||||||
|
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
|
||||||
|
() => Is.EquivalentTo(new Colour4[]
|
||||||
|
{
|
||||||
|
SkinConfiguration.DefaultComboColours[1],
|
||||||
|
SkinConfiguration.DefaultComboColours[2],
|
||||||
|
SkinConfiguration.DefaultComboColours[3],
|
||||||
|
SkinConfiguration.DefaultComboColours[0],
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
|
||||||
|
AddAssert("beatmap skin has colours",
|
||||||
|
() => skin.Configuration.CustomComboColours,
|
||||||
|
() => Is.EquivalentTo(new[]
|
||||||
|
{
|
||||||
|
SkinConfiguration.DefaultComboColours[1],
|
||||||
|
SkinConfiguration.DefaultComboColours[2],
|
||||||
|
SkinConfiguration.DefaultComboColours[3],
|
||||||
|
Color4.Aqua,
|
||||||
|
SkinConfiguration.DefaultComboColours[0],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExistingColours()
|
||||||
|
{
|
||||||
|
LegacyBeatmapSkin skin = null!;
|
||||||
|
ColoursSection coloursSection = null!;
|
||||||
|
|
||||||
|
AddStep("create beatmap skin", () =>
|
||||||
|
{
|
||||||
|
skin = new LegacyBeatmapSkin(new BeatmapInfo(), null);
|
||||||
|
skin.Configuration.CustomComboColours = new List<Color4>
|
||||||
|
{
|
||||||
|
Color4.Azure,
|
||||||
|
Color4.Beige,
|
||||||
|
Color4.Chartreuse
|
||||||
|
};
|
||||||
|
});
|
||||||
|
AddStep("create colours section", () => Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
|
||||||
|
}, skin)),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
|
||||||
|
],
|
||||||
|
Child = coloursSection = new ColoursSection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AddAssert("section displays combo colours",
|
||||||
|
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
|
||||||
|
() => Is.EquivalentTo(new[]
|
||||||
|
{
|
||||||
|
Colour4.Beige,
|
||||||
|
Colour4.Chartreuse,
|
||||||
|
Colour4.Azure,
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
|
||||||
|
AddAssert("beatmap skin has colours",
|
||||||
|
() => skin.Configuration.CustomComboColours,
|
||||||
|
() => Is.EquivalentTo(new[]
|
||||||
|
{
|
||||||
|
Color4.Azure,
|
||||||
|
Color4.Beige,
|
||||||
|
Color4.Aqua,
|
||||||
|
Color4.Chartreuse
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -84,6 +84,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
targetContainer = getTargetContainer();
|
targetContainer = getTargetContainer();
|
||||||
initialRotation = targetContainer!.Rotation;
|
initialRotation = targetContainer!.Rotation;
|
||||||
|
DefaultOrigin = ToLocalSpace(targetContainer.ToScreenSpace(Vector2.Zero));
|
||||||
|
|
||||||
base.Begin();
|
base.Begin();
|
||||||
}
|
}
|
||||||
@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
|
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||||
{
|
{
|
||||||
if (targetContainer == null)
|
if (targetContainer == null)
|
||||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -36,6 +38,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private ContextMenuContainer contextMenuContainer
|
private ContextMenuContainer contextMenuContainer
|
||||||
=> Editor.ChildrenOfType<ContextMenuContainer>().First();
|
=> Editor.ChildrenOfType<ContextMenuContainer>().First();
|
||||||
|
|
||||||
|
private SelectionBoxScaleHandle getScaleHandle(Anchor anchor)
|
||||||
|
=> Editor.ChildrenOfType<SelectionBoxScaleHandle>().First(it => it.Anchor == anchor);
|
||||||
|
|
||||||
private void moveMouseToObject(Func<HitObject> targetFunc)
|
private void moveMouseToObject(Func<HitObject> targetFunc)
|
||||||
{
|
{
|
||||||
AddStep("move mouse to object", () =>
|
AddStep("move mouse to object", () =>
|
||||||
@ -78,7 +83,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNudgeSelection()
|
public void TestNudgeSelectionTime()
|
||||||
{
|
{
|
||||||
HitCircle[] addedObjects = null!;
|
HitCircle[] addedObjects = null!;
|
||||||
|
|
||||||
@ -99,6 +104,51 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
|
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNudgeSelectionPosition()
|
||||||
|
{
|
||||||
|
HitCircle addedObject = null!;
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
|
||||||
|
{
|
||||||
|
addedObject = new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("select object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("nudge up", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Up);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved up", () => addedObject.Position.Y, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
AddStep("nudge down", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Down);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved down", () => addedObject.Position.Y, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
AddStep("nudge left", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved left", () => addedObject.Position.X, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
AddStep("nudge right", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Right);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddAssert("object position moved right", () => addedObject.Position.X, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRotateHotkeys()
|
public void TestRotateHotkeys()
|
||||||
{
|
{
|
||||||
@ -215,6 +265,51 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiSelectWithDragBox()
|
||||||
|
{
|
||||||
|
var addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100 },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||||
|
new HitCircle { StartTime = 300, Position = new Vector2(512, 0) },
|
||||||
|
new HitCircle { StartTime = 400, Position = new Vector2(412, 100) },
|
||||||
|
};
|
||||||
|
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
|
||||||
|
|
||||||
|
AddStep("start dragging", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopLeft - new Vector2(5)));
|
||||||
|
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||||
|
|
||||||
|
AddStep("start dragging with control", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||||
|
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
|
||||||
|
|
||||||
|
AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4));
|
||||||
|
|
||||||
|
AddStep("start dragging without control", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||||
|
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNearestSelection()
|
public void TestNearestSelection()
|
||||||
{
|
{
|
||||||
@ -519,5 +614,137 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShiftModifierMaintainsAspectRatio()
|
||||||
|
{
|
||||||
|
HitCircle[] addedObjects = null!;
|
||||||
|
|
||||||
|
float aspectRatioBeforeDrag = 0;
|
||||||
|
|
||||||
|
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.AddRange(addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||||
|
});
|
||||||
|
|
||||||
|
aspectRatioBeforeDrag = getAspectRatio();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||||
|
|
||||||
|
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||||
|
|
||||||
|
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||||
|
|
||||||
|
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
|
|
||||||
|
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||||
|
|
||||||
|
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAltModifierScalesAroundCenter()
|
||||||
|
{
|
||||||
|
HitCircle[] addedObjects = null!;
|
||||||
|
|
||||||
|
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||||
|
|
||||||
|
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.AddRange(addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||||
|
});
|
||||||
|
|
||||||
|
centerBeforeDrag = getCenter();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||||
|
|
||||||
|
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||||
|
|
||||||
|
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||||
|
|
||||||
|
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||||
|
|
||||||
|
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||||
|
|
||||||
|
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestShiftAndAltModifierKeys()
|
||||||
|
{
|
||||||
|
HitCircle[] addedObjects = null!;
|
||||||
|
|
||||||
|
float aspectRatioBeforeDrag = 0;
|
||||||
|
|
||||||
|
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||||
|
|
||||||
|
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||||
|
|
||||||
|
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||||
|
|
||||||
|
AddStep("add hitobjects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.AddRange(addedObjects = new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||||
|
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||||
|
});
|
||||||
|
|
||||||
|
aspectRatioBeforeDrag = getAspectRatio();
|
||||||
|
centerBeforeDrag = getCenter();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||||
|
|
||||||
|
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||||
|
|
||||||
|
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||||
|
|
||||||
|
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||||
|
|
||||||
|
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
|
|
||||||
|
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||||
|
|
||||||
|
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||||
|
|
||||||
|
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||||
|
|
||||||
|
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||||
|
|
||||||
|
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
|
|
||||||
|
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,14 @@ using System;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private TestDesignSection designSection;
|
private TestDesignSection designSection;
|
||||||
private EditorBeatmap editorBeatmap { get; set; }
|
private EditorBeatmap editorBeatmap { get; set; }
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUp()
|
public void SetUp()
|
||||||
{
|
{
|
||||||
@ -42,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
(typeof(EditorBeatmap), editorBeatmap)
|
(typeof(EditorBeatmap), editorBeatmap)
|
||||||
},
|
},
|
||||||
Child = designSection = new TestDesignSection()
|
Child = designSection = new TestDesignSection { RelativeSizeAxes = Axes.X }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,11 +104,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private partial class TestDesignSection : DesignSection
|
private partial class TestDesignSection : DesignSection
|
||||||
{
|
{
|
||||||
public new LabelledSwitchButton EnableCountdown => base.EnableCountdown;
|
public new FormCheckBox EnableCountdown => base.EnableCountdown;
|
||||||
|
|
||||||
public new FillFlowContainer CountdownSettings => base.CountdownSettings;
|
public new FillFlowContainer CountdownSettings => base.CountdownSettings;
|
||||||
public new LabelledEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
public new FormEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
|
||||||
public new LabelledNumberBox CountdownOffset => base.CountdownOffset;
|
public new FormTextBox CountdownOffset => base.CountdownOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Components.Menus;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
|
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||||
|
|
||||||
if (i == 11)
|
if (i == 11)
|
||||||
{
|
{
|
||||||
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
EditorBeatmap.EndChange();
|
EditorBeatmap.EndChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||||
|
|
||||||
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
||||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
||||||
|
@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
|
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
|
||||||
|
|
||||||
public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
|
public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
@ -135,9 +137,42 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
pressAndCheckTime(Key.Up, 0);
|
pressAndCheckTime(Key.Up, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pressAndCheckTime(Key key, double expectedTime)
|
[Test]
|
||||||
|
public void TestSeekBetweenObjects()
|
||||||
{
|
{
|
||||||
AddStep($"press {key}", () => InputManager.Key(key));
|
AddStep("add objects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Clear();
|
||||||
|
EditorBeatmap.AddRange(new[]
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1000, },
|
||||||
|
new HitCircle { StartTime = 2250, },
|
||||||
|
new HitCircle { StartTime = 3600, },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
AddStep("seek to 0", () => EditorClock.Seek(0));
|
||||||
|
|
||||||
|
pressAndCheckTime(Key.Right, 1000, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Right, 2250, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Left, 2250, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
|
||||||
|
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pressAndCheckTime(Key key, double expectedTime, params Key[] modifiers)
|
||||||
|
{
|
||||||
|
AddStep($"press {key} with {(modifiers.Any() ? string.Join(',', modifiers) : "no modifiers")}", () =>
|
||||||
|
{
|
||||||
|
foreach (var modifier in modifiers)
|
||||||
|
InputManager.PressKey(modifier);
|
||||||
|
|
||||||
|
InputManager.Key(key);
|
||||||
|
|
||||||
|
foreach (var modifier in modifiers)
|
||||||
|
InputManager.ReleaseKey(modifier);
|
||||||
|
});
|
||||||
AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
|
AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
@ -20,6 +22,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
|
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
|
||||||
{
|
{
|
||||||
@ -201,7 +206,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createSection()
|
private void createSection()
|
||||||
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
|
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection { RelativeSizeAxes = Axes.X });
|
||||||
|
|
||||||
private void assertArtistMetadata(string expected)
|
private void assertArtistMetadata(string expected)
|
||||||
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
|
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
|
||||||
@ -226,11 +231,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private partial class TestMetadataSection : MetadataSection
|
private partial class TestMetadataSection : MetadataSection
|
||||||
{
|
{
|
||||||
public new LabelledTextBox ArtistTextBox => base.ArtistTextBox;
|
public new FormTextBox ArtistTextBox => base.ArtistTextBox;
|
||||||
public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
public new FormTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
|
||||||
|
|
||||||
public new LabelledTextBox TitleTextBox => base.TitleTextBox;
|
public new FormTextBox TitleTextBox => base.TitleTextBox;
|
||||||
public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
public new FormTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,20 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
assertOnScreenAt(EditorScreenMode.Compose, 0);
|
assertOnScreenAt(EditorScreenMode.Compose, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUrlDecodingOfArgs()
|
||||||
|
{
|
||||||
|
setUpEditor(new OsuRuleset().RulesetInfo);
|
||||||
|
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(new OsuRuleset().RulesetInfo));
|
||||||
|
|
||||||
|
AddStep("jump to encoded link", () => Game.HandleLink("osu://edit/00:14:142%20(1)"));
|
||||||
|
|
||||||
|
AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value);
|
||||||
|
|
||||||
|
AddAssert("time is correct", () => editorClock.CurrentTime, () => Is.EqualTo(14_142));
|
||||||
|
AddAssert("selected object is correct", () => editorBeatmap.SelectedHitObjects.Single().StartTime, () => Is.EqualTo(14_142));
|
||||||
|
}
|
||||||
|
|
||||||
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
|
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
|
||||||
{
|
{
|
||||||
AddStep($"{step} {timestamp}", () =>
|
AddStep($"{step} {timestamp}", () =>
|
||||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.UI;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens;
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osu.Game.Tests.Gameplay;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Visual.Multiplayer;
|
using osu.Game.Tests.Visual.Multiplayer;
|
||||||
@ -167,14 +168,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
public void TestSpectatingDuringGameplay()
|
public void TestSpectatingDuringGameplay()
|
||||||
{
|
{
|
||||||
start();
|
start();
|
||||||
sendFrames(300);
|
sendFrames(300, initialResultCount: 100);
|
||||||
|
|
||||||
loadSpectatingScreen();
|
loadSpectatingScreen();
|
||||||
waitForPlayerCurrent();
|
waitForPlayerCurrent();
|
||||||
|
|
||||||
sendFrames(300);
|
sendFrames(300, initialResultCount: 100);
|
||||||
|
|
||||||
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000));
|
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000));
|
||||||
|
AddAssert("check judgement counts are correct", () => player.ChildrenOfType<JudgementCountController>().Single().Counters.Sum(c => c.ResultCount.Value),
|
||||||
|
() => Is.GreaterThanOrEqualTo(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private void checkPaused(bool state) =>
|
private void checkPaused(bool state) =>
|
||||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||||
|
|
||||||
private void sendFrames(int count = 10, double startTime = 0)
|
private void sendFrames(int count = 10, double startTime = 0, int initialResultCount = 0)
|
||||||
{
|
{
|
||||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
|
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime, initialResultCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSpectatingScreen()
|
private void loadSpectatingScreen()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user