mirror of
https://github.com/ppy/osu.git
synced 2025-01-31 11:33:22 +08:00
Merge branch 'master' into velopack
This commit is contained in:
commit
04c8df065c
@ -21,7 +21,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2023.1117.0",
|
"version": "2024.517.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"localisation"
|
||||||
]
|
]
|
||||||
|
@ -196,6 +196,9 @@ csharp_style_prefer_switch_expression = false:none
|
|||||||
|
|
||||||
csharp_style_namespace_declarations = block_scoped:warning
|
csharp_style_namespace_declarations = block_scoped:warning
|
||||||
|
|
||||||
|
#Style - C# 12 features
|
||||||
|
csharp_style_prefer_primary_constructors = false
|
||||||
|
|
||||||
[*.{yaml,yml}]
|
[*.{yaml,yml}]
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -265,6 +265,7 @@ __pycache__/
|
|||||||
.idea/**/usage.statistics.xml
|
.idea/**/usage.statistics.xml
|
||||||
.idea/**/dictionaries
|
.idea/**/dictionaries
|
||||||
.idea/**/shelf
|
.idea/**/shelf
|
||||||
|
.idea/*/.idea/projectSettingsUpdater.xml
|
||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
.idea/**/contentModel.xml
|
.idea/**/contentModel.xml
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RiderProjectSettingsUpdater">
|
|
||||||
<option name="vcsConfiguration" value="2" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RiderProjectSettingsUpdater">
|
|
||||||
<option name="vcsConfiguration" value="2" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RiderProjectSettingsUpdater">
|
|
||||||
<option name="vcsConfiguration" value="2" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RiderProjectSettingsUpdater">
|
|
||||||
<option name="vcsConfiguration" value="2" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -2,7 +2,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup Label="C#">
|
<PropertyGroup Label="C#">
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>12.0</LangVersion>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.509.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.625.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.
|
||||||
|
@ -164,8 +164,8 @@ namespace osu.Desktop
|
|||||||
// user activity
|
// user activity
|
||||||
if (activity.Value != null)
|
if (activity.Value != null)
|
||||||
{
|
{
|
||||||
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
|
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
|
||||||
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||||
|
|
||||||
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
|
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
|
||||||
{
|
{
|
||||||
@ -271,8 +271,19 @@ namespace osu.Desktop
|
|||||||
|
|
||||||
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
|
||||||
|
|
||||||
private static string truncate(string str)
|
private static string clampLength(string str)
|
||||||
{
|
{
|
||||||
|
// Empty strings are fine to discord even though single-character strings are not. Make it make sense.
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return str;
|
||||||
|
|
||||||
|
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
|
||||||
|
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
|
||||||
|
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
||||||
|
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
|
||||||
|
if (str.Length < 2)
|
||||||
|
return str.PadRight(2, '\u200B');
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
||||||
return str;
|
return str;
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ using osu.Game.IO;
|
|||||||
using osu.Game.IPC;
|
using osu.Game.IPC;
|
||||||
using osu.Game.Performance;
|
using osu.Game.Performance;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using SDL;
|
|
||||||
|
|
||||||
namespace osu.Desktop
|
namespace osu.Desktop
|
||||||
{
|
{
|
||||||
@ -152,7 +151,7 @@ namespace osu.Desktop
|
|||||||
host.Window.Title = Name;
|
host.Window.Title = Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo();
|
protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo();
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
@ -160,24 +159,5 @@ namespace osu.Desktop
|
|||||||
osuSchemeLinkIPCChannel?.Dispose();
|
osuSchemeLinkIPCChannel?.Dispose();
|
||||||
archiveImportIPCChannel?.Dispose();
|
archiveImportIPCChannel?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe class SDL3BatteryInfo : BatteryInfo
|
|
||||||
{
|
|
||||||
public override double? ChargeLevel
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
int percentage;
|
|
||||||
SDL3.SDL_GetPowerInfo(null, &percentage);
|
|
||||||
|
|
||||||
if (percentage == -1)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return percentage / 100.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
osu.Desktop/SDL2BatteryInfo.cs
Normal file
25
osu.Desktop/SDL2BatteryInfo.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
|
namespace osu.Desktop
|
||||||
|
{
|
||||||
|
internal class SDL2BatteryInfo : BatteryInfo
|
||||||
|
{
|
||||||
|
public override double? ChargeLevel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage);
|
||||||
|
|
||||||
|
if (percentage == -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return percentage / 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
||||||
|
}
|
||||||
|
}
|
27
osu.Desktop/SDL3BatteryInfo.cs
Normal file
27
osu.Desktop/SDL3BatteryInfo.cs
Normal file
@ -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 osu.Game.Utils;
|
||||||
|
using SDL;
|
||||||
|
|
||||||
|
namespace osu.Desktop
|
||||||
|
{
|
||||||
|
internal unsafe class SDL3BatteryInfo : BatteryInfo
|
||||||
|
{
|
||||||
|
public override double? ChargeLevel
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
int percentage;
|
||||||
|
SDL3.SDL_GetPowerInfo(null, &percentage);
|
||||||
|
|
||||||
|
if (percentage == -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return percentage / 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||||
[TestCase("112643")]
|
[TestCase("112643")]
|
||||||
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
||||||
|
[TestCase("high-speed-multiplier-precision")]
|
||||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||||
|
|
||||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||||
|
@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
|||||||
StartTime = 5000,
|
StartTime = 5000,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Breaks = new List<BreakPeriod>
|
Breaks =
|
||||||
{
|
{
|
||||||
new BreakPeriod(2000, 4000),
|
new BreakPeriod(2000, 4000),
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
{"Mappings":[{"StartTime":265568.0,"Objects":[{"StartTime":265568.0,"Position":486.0,"HyperDash":false},{"StartTime":265658.0,"Position":465.1873,"HyperDash":false},{"StartTime":265749.0,"Position":463.208435,"HyperDash":false},{"StartTime":265840.0,"Position":465.146484,"HyperDash":false},{"StartTime":265967.0,"Position":459.5862,"HyperDash":false}]}]}
|
@ -0,0 +1,238 @@
|
|||||||
|
osu file format v14
|
||||||
|
|
||||||
|
[General]
|
||||||
|
AudioFilename: audio.mp3
|
||||||
|
AudioLeadIn: 0
|
||||||
|
PreviewTime: 226943
|
||||||
|
Countdown: 0
|
||||||
|
SampleSet: Soft
|
||||||
|
StackLeniency: 0.7
|
||||||
|
Mode: 2
|
||||||
|
LetterboxInBreaks: 0
|
||||||
|
WidescreenStoryboard: 1
|
||||||
|
|
||||||
|
[Editor]
|
||||||
|
Bookmarks: 85568,86768,90968,265568
|
||||||
|
DistanceSpacing: 0.9
|
||||||
|
BeatDivisor: 12
|
||||||
|
GridSize: 16
|
||||||
|
TimelineZoom: 1
|
||||||
|
|
||||||
|
[Metadata]
|
||||||
|
Title:Snow
|
||||||
|
TitleUnicode:Snow
|
||||||
|
Artist:Ricky Montgomery
|
||||||
|
ArtistUnicode:Ricky Montgomery
|
||||||
|
Creator:Crowley
|
||||||
|
Version:Bury Me Six Feet in Snow
|
||||||
|
Source:
|
||||||
|
Tags:indie the honeysticks alternative english
|
||||||
|
BeatmapID:2062131
|
||||||
|
BeatmapSetID:971028
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:6
|
||||||
|
CircleSize:4.2
|
||||||
|
OverallDifficulty:8.3
|
||||||
|
ApproachRate:8.3
|
||||||
|
SliderMultiplier:3.59999990463257
|
||||||
|
SliderTickRate:1
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
//Background and Video events
|
||||||
|
0,0,"me.jpg",0,0
|
||||||
|
//Break Periods
|
||||||
|
//Storyboard Layer 0 (Background)
|
||||||
|
//Storyboard Layer 1 (Fail)
|
||||||
|
//Storyboard Layer 2 (Pass)
|
||||||
|
//Storyboard Layer 3 (Foreground)
|
||||||
|
//Storyboard Layer 4 (Overlay)
|
||||||
|
//Storyboard Sound Samples
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
368,1200,2,2,1,30,1,0
|
||||||
|
368,-66.6666666666667,2,2,1,30,0,0
|
||||||
|
29168,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
30368,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
30568,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
31368,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
31568,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
32768,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
33568,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
33968,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
35168,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
35968,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
36168,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
36368,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
37568,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
37968,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
38368,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
38768,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
39968,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
40168,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
40968,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
41168,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
42368,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
43168,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
43568,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
44768,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
45768,-58.8235294117647,2,2,2,40,0,0
|
||||||
|
45968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
47168,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
48368,-62.5,2,2,1,50,0,0
|
||||||
|
67568,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
84668,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
84768,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
85068,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
85168,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
85468,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
85568,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
86768,-58.8235294117647,2,2,1,30,0,0
|
||||||
|
91168,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
91568,1200,2,2,1,50,1,0
|
||||||
|
91568,-58.8235294117647,2,2,1,50,0,1
|
||||||
|
91643,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
92768,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
92968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
95168,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
95368,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
97568,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
97768,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
99968,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
100168,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
100768,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
101168,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
102368,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
102568,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
104768,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
104968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
107168,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
107368,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
108968,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
109168,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
109568,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
109968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
110368,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
110768,-100,2,2,1,40,0,0
|
||||||
|
127568,-62.5,2,2,2,50,0,0
|
||||||
|
127968,-62.5,2,2,1,50,0,0
|
||||||
|
128168,-62.5,2,2,2,50,0,0
|
||||||
|
129968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
131168,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
131368,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
133568,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
133768,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
135968,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
136168,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
138368,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
138568,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
139168,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
139368,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
139568,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
140768,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
140968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
143168,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
143368,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
145568,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
145768,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
147368,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
147768,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
147968,-58.8235294117647,2,2,1,60,0,0
|
||||||
|
148768,-58.8235294117647,2,2,2,60,0,0
|
||||||
|
149168,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
158268,-58.8235294117647,2,2,2,70,0,1
|
||||||
|
158568,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
166268,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
166368,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
166668,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
166768,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
167068,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
167168,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
168368,-62.5,2,2,1,50,0,0
|
||||||
|
172368,-62.5,2,2,1,50,0,1
|
||||||
|
173168,-62.5,2,2,1,50,0,0
|
||||||
|
185168,-62.5,2,2,1,60,0,0
|
||||||
|
185468,-62.5,2,2,1,5,0,0
|
||||||
|
185568,-62.5,2,2,1,60,0,0
|
||||||
|
185868,-62.5,2,2,1,5,0,0
|
||||||
|
185968,-62.5,2,2,1,60,0,0
|
||||||
|
186268,-62.5,2,2,1,5,0,0
|
||||||
|
186368,-62.5,2,2,1,60,0,0
|
||||||
|
186668,-62.5,2,2,1,5,0,0
|
||||||
|
186768,-52.6315789473684,2,2,1,60,0,0
|
||||||
|
187068,-62.5,2,2,1,5,0,0
|
||||||
|
187168,-62.5,2,2,1,60,0,0
|
||||||
|
187468,-62.5,2,2,1,5,0,0
|
||||||
|
187568,-62.5,2,2,1,20,0,0
|
||||||
|
187768,-62.5,2,2,1,24,0,0
|
||||||
|
187968,-62.5,2,2,1,28,0,0
|
||||||
|
188168,-62.5,2,2,1,32,0,0
|
||||||
|
188368,-62.5,2,2,1,36,0,0
|
||||||
|
188568,-62.5,2,2,1,40,0,0
|
||||||
|
188768,1200,2,2,1,50,1,1
|
||||||
|
188768,-58.8235294117647,2,2,1,50,0,1
|
||||||
|
188843,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
189968,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
190168,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
192368,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
192568,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
194768,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
194968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
196568,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
196768,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
197168,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
197368,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
197568,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
197968,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
198368,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
199568,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
199768,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
201968,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
202168,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
204368,-58.8235294117647,2,2,2,50,0,0
|
||||||
|
204568,-58.8235294117647,2,2,1,50,0,0
|
||||||
|
206768,-58.8235294117647,2,2,1,60,0,0
|
||||||
|
207168,-58.8235294117647,2,2,2,60,0,0
|
||||||
|
207968,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
216968,-58.8235294117647,2,2,2,70,0,1
|
||||||
|
217168,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
217368,-58.8235294117647,2,2,2,70,0,1
|
||||||
|
217568,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
225068,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
225168,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
225468,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
225568,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
225868,-58.8235294117647,2,2,1,5,0,1
|
||||||
|
225968,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
227168,-58.8235294117647,2,2,1,30,0,0
|
||||||
|
234368,-58.8235294117647,2,2,1,40,0,0
|
||||||
|
236768,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
255968,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
261168,-58.8235294117647,2,2,1,70,0,1
|
||||||
|
263068,-58.8235294117647,2,2,1,70,0,0
|
||||||
|
263168,-58.8235294117647,2,2,1,60,0,1
|
||||||
|
263243,-58.8235294117647,2,2,1,60,0,0
|
||||||
|
264368,-58.8235294117647,2,2,1,60,0,1
|
||||||
|
264443,-58.8235294117647,2,2,1,60,0,0
|
||||||
|
265568,-444.444444444444,2,2,1,50,0,1
|
||||||
|
265643,-444.444444444444,2,2,1,50,0,0
|
||||||
|
266768,-444.444444444444,2,2,1,40,0,0
|
||||||
|
267968,-444.444444444444,2,2,1,30,0,0
|
||||||
|
269168,-444.444444444444,2,2,1,20,0,0
|
||||||
|
270368,-444.444444444444,2,2,1,10,0,0
|
||||||
|
271168,-444.444444444444,2,2,1,9,0,0
|
||||||
|
271568,-444.444444444444,2,2,1,8,0,0
|
||||||
|
271968,-444.444444444444,2,2,1,7,0,0
|
||||||
|
272368,-444.444444444444,2,2,1,6,0,0
|
||||||
|
272768,-444.444444444444,2,2,1,5,0,0
|
||||||
|
275168,-444.444444444444,2,2,1,5,0,0
|
||||||
|
|
||||||
|
|
||||||
|
[Colours]
|
||||||
|
Combo1 : 255,128,128
|
||||||
|
Combo2 : 72,72,255
|
||||||
|
Combo3 : 192,192,192
|
||||||
|
Combo4 : 255,136,79
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
486,179,265568,6,0,P|461:174|454:174,1,26.999997997284,6|0,1:2|0:0,0:0:0:0:
|
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
value *= Math.Pow(accuracy(), 5.5);
|
value *= Math.Pow(accuracy(), 5.5);
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModNoFail))
|
if (score.Mods.Any(m => m is ModNoFail))
|
||||||
value *= 0.90;
|
value *= Math.Max(0.90, 1.0 - 0.02 * numMiss);
|
||||||
|
|
||||||
return new CatchPerformanceAttributes
|
return new CatchPerformanceAttributes
|
||||||
{
|
{
|
||||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
inputManager = GetContainingInputManager();
|
inputManager = GetContainingInputManager()!;
|
||||||
|
|
||||||
BeginPlacement();
|
BeginPlacement();
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||||
{
|
{
|
||||||
Precision = 0.01,
|
|
||||||
MinValue = 0.1,
|
MinValue = 0.1,
|
||||||
MaxValue = 10
|
MaxValue = 10
|
||||||
};
|
};
|
||||||
@ -77,6 +76,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
base.CreateNestedHitObjects(cancellationToken);
|
base.CreateNestedHitObjects(cancellationToken);
|
||||||
|
|
||||||
|
this.PopulateNodeSamples();
|
||||||
|
|
||||||
var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
|
var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
|
||||||
|
|
||||||
int nodeIndex = 0;
|
int nodeIndex = 0;
|
||||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
|
var replayState = (GetContainingInputManager()!.CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
|
||||||
|
|
||||||
SetCatcherPosition(
|
SetCatcherPosition(
|
||||||
replayState?.CatcherX ??
|
replayState?.CatcherX ??
|
||||||
|
@ -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 System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
public partial class TestHitObjectComposer : HitObjectComposer
|
public partial class TestHitObjectComposer : HitObjectComposer
|
||||||
{
|
{
|
||||||
public override Playfield Playfield { get; }
|
public override Playfield Playfield { get; }
|
||||||
|
public override ComposeBlueprintContainer BlueprintContainer => throw new NotImplementedException();
|
||||||
public override IEnumerable<DrawableHitObject> HitObjects => Enumerable.Empty<DrawableHitObject>();
|
public override IEnumerable<DrawableHitObject> HitObjects => Enumerable.Empty<DrawableHitObject>();
|
||||||
public override bool CursorInPlacementArea => false;
|
public override bool CursorInPlacementArea => false;
|
||||||
|
|
||||||
@ -100,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||||
|
{
|
||||||
|
public partial class TestSceneManiaEditorSaving : EditorSavingTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestKeyCountChange()
|
||||||
|
{
|
||||||
|
LabelledSliderBar<float> keyCount = null!;
|
||||||
|
|
||||||
|
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||||
|
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<LabelledSliderBar<float>>().First(), () => Is.Not.Null);
|
||||||
|
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||||
|
AddStep("change key count to 8", () =>
|
||||||
|
{
|
||||||
|
keyCount.Current.Value = 8;
|
||||||
|
});
|
||||||
|
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||||
|
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||||
|
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||||
|
|
||||||
|
AddStep("change key count to 8 again", () =>
|
||||||
|
{
|
||||||
|
keyCount.Current.Value = 8;
|
||||||
|
});
|
||||||
|
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||||
|
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||||
|
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -186,8 +186,106 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
||||||
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
||||||
|
|
||||||
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
|
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
|
||||||
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
|
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDragHoldNoteHead()
|
||||||
|
{
|
||||||
|
setScrollStep(ScrollingDirection.Down);
|
||||||
|
|
||||||
|
HoldNote holdNote = null;
|
||||||
|
AddStep("setup beatmap", () =>
|
||||||
|
{
|
||||||
|
composer.EditorBeatmap.Clear();
|
||||||
|
composer.EditorBeatmap.Add(holdNote = new HoldNote
|
||||||
|
{
|
||||||
|
Column = 1,
|
||||||
|
StartTime = 250,
|
||||||
|
EndTime = 750,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
DrawableHoldNote drawableHoldNote = null;
|
||||||
|
EditHoldNoteEndPiece headPiece = null;
|
||||||
|
|
||||||
|
AddStep("select blueprint", () =>
|
||||||
|
{
|
||||||
|
drawableHoldNote = this.ChildrenOfType<DrawableHoldNote>().Single();
|
||||||
|
InputManager.MoveMouseTo(drawableHoldNote);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("grab hold note head", () =>
|
||||||
|
{
|
||||||
|
headPiece = this.ChildrenOfType<EditHoldNoteEndPiece>().First();
|
||||||
|
InputManager.MoveMouseTo(headPiece);
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("drag head downwards", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(headPiece, new Vector2(0, 100));
|
||||||
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("start time moved back", () => holdNote!.StartTime, () => Is.LessThan(250));
|
||||||
|
AddAssert("end time unchanged", () => holdNote.EndTime, () => Is.EqualTo(750));
|
||||||
|
|
||||||
|
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
||||||
|
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
||||||
|
|
||||||
|
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
|
||||||
|
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDragHoldNoteTail()
|
||||||
|
{
|
||||||
|
setScrollStep(ScrollingDirection.Down);
|
||||||
|
|
||||||
|
HoldNote holdNote = null;
|
||||||
|
AddStep("setup beatmap", () =>
|
||||||
|
{
|
||||||
|
composer.EditorBeatmap.Clear();
|
||||||
|
composer.EditorBeatmap.Add(holdNote = new HoldNote
|
||||||
|
{
|
||||||
|
Column = 1,
|
||||||
|
StartTime = 250,
|
||||||
|
EndTime = 750,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
DrawableHoldNote drawableHoldNote = null;
|
||||||
|
EditHoldNoteEndPiece tailPiece = null;
|
||||||
|
|
||||||
|
AddStep("select blueprint", () =>
|
||||||
|
{
|
||||||
|
drawableHoldNote = this.ChildrenOfType<DrawableHoldNote>().Single();
|
||||||
|
InputManager.MoveMouseTo(drawableHoldNote);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddStep("grab hold note tail", () =>
|
||||||
|
{
|
||||||
|
tailPiece = this.ChildrenOfType<EditHoldNoteEndPiece>().Last();
|
||||||
|
InputManager.MoveMouseTo(tailPiece);
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("drag tail upwards", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(tailPiece, new Vector2(0, -100));
|
||||||
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("start time unchanged", () => holdNote!.StartTime, () => Is.EqualTo(250));
|
||||||
|
AddAssert("end time moved forward", () => holdNote.EndTime, () => Is.GreaterThan(750));
|
||||||
|
|
||||||
|
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.BottomLeft, drawableHoldNote.Head.ScreenSpaceDrawQuad.BottomLeft));
|
||||||
|
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(drawableHoldNote.ScreenSpaceDrawQuad.TopLeft, drawableHoldNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
|
||||||
|
|
||||||
|
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(0).DrawPosition == drawableHoldNote.Head.DrawPosition);
|
||||||
|
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditHoldNoteEndPiece>().ElementAt(1).DrawPosition == drawableHoldNote.Tail.DrawPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setScrollStep(ScrollingDirection direction)
|
private void setScrollStep(ScrollingDirection direction)
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||||
|
{
|
||||||
|
public partial class TestSceneManiaSelectionHandler : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHorizontalFlipOverSelection()
|
||||||
|
{
|
||||||
|
ManiaHitObject first = null!, second = null!, third = null!;
|
||||||
|
|
||||||
|
AddStep("create objects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
|
||||||
|
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
|
||||||
|
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
AddStep("flip horizontally over selection", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxButton>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("first object stayed in place", () => first.Column, () => Is.EqualTo(2));
|
||||||
|
AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(3));
|
||||||
|
AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHorizontalFlipOverPlayfield()
|
||||||
|
{
|
||||||
|
ManiaHitObject first = null!, second = null!, third = null!;
|
||||||
|
|
||||||
|
AddStep("create objects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
|
||||||
|
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
|
||||||
|
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
AddStep("flip horizontally", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.H);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("first object flipped", () => first.Column, () => Is.EqualTo(1));
|
||||||
|
AddAssert("second object flipped", () => second.Column, () => Is.EqualTo(2));
|
||||||
|
AddAssert("third object flipped", () => third.Column, () => Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVerticalFlip()
|
||||||
|
{
|
||||||
|
ManiaHitObject first = null!, second = null!, third = null!;
|
||||||
|
|
||||||
|
AddStep("create objects", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Add(first = new Note { StartTime = 250, Column = 2 });
|
||||||
|
EditorBeatmap.Add(second = new HoldNote { StartTime = 750, Duration = 1500, Column = 1 });
|
||||||
|
EditorBeatmap.Add(third = new Note { StartTime = 1250, Column = 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||||
|
AddStep("flip vertically", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.J);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("first object flipped", () => first.StartTime, () => Is.EqualTo(2250));
|
||||||
|
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
|
||||||
|
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
AddStep("Hold key", () =>
|
AddStep("Hold key", () =>
|
||||||
{
|
{
|
||||||
clock.CurrentTime = 0;
|
clock.CurrentTime = 0;
|
||||||
note.OnPressed(new KeyBindingPressEvent<ManiaAction>(GetContainingInputManager().CurrentState, ManiaAction.Key1));
|
note.OnPressed(new KeyBindingPressEvent<ManiaAction>(GetContainingInputManager()!.CurrentState, ManiaAction.Key1));
|
||||||
});
|
});
|
||||||
AddStep("progress time", () => clock.CurrentTime = 500);
|
AddStep("progress time", () => clock.CurrentTime = 500);
|
||||||
AddAssert("head is visible", () => note.Head.Alpha == 1);
|
AddAssert("head is visible", () => note.Head.Alpha == 1);
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTouchAreaNotInitiallyVisible()
|
||||||
|
{
|
||||||
|
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPressReceptors()
|
||||||
|
{
|
||||||
|
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int index = i;
|
||||||
|
|
||||||
|
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("action sent",
|
||||||
|
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||||
|
() => Does.Contain(getReceptor(index).Action.Value));
|
||||||
|
|
||||||
|
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||||
|
|
||||||
|
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||||
|
|
||||||
|
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public partial class EditHoldNoteEndPiece : CompositeDrawable
|
||||||
|
{
|
||||||
|
public Action? DragStarted { get; init; }
|
||||||
|
public Action<Vector2>? Dragging { get; init; }
|
||||||
|
public Action? DragEnded { get; init; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Height = DefaultNotePiece.NOTE_HEIGHT;
|
||||||
|
|
||||||
|
CornerRadius = 5;
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
InternalChild = new DefaultNotePiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
updateState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
updateState();
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
|
{
|
||||||
|
DragStarted?.Invoke();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDrag(DragEvent e)
|
||||||
|
{
|
||||||
|
base.OnDrag(e);
|
||||||
|
Dragging?.Invoke(e.ScreenSpaceMousePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
{
|
||||||
|
base.OnDragEnd(e);
|
||||||
|
DragEnded?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
var colour = colours.Yellow;
|
||||||
|
|
||||||
|
if (IsHovered)
|
||||||
|
colour = colour.Lighten(1);
|
||||||
|
|
||||||
|
Colour = colour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
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.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
@ -18,10 +18,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
|
public partial class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
private EditNotePiece head;
|
[Resolved]
|
||||||
private EditNotePiece tail;
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap? editorBeatmap { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||||
|
|
||||||
|
private EditHoldNoteEndPiece head = null!;
|
||||||
|
private EditHoldNoteEndPiece tail = null!;
|
||||||
|
|
||||||
public HoldNoteSelectionBlueprint(HoldNote hold)
|
public HoldNoteSelectionBlueprint(HoldNote hold)
|
||||||
: base(hold)
|
: base(hold)
|
||||||
@ -33,8 +42,43 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
head = new EditNotePiece { RelativeSizeAxes = Axes.X },
|
head = new EditHoldNoteEndPiece
|
||||||
tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
DragStarted = () => changeHandler?.BeginChange(),
|
||||||
|
Dragging = pos =>
|
||||||
|
{
|
||||||
|
double endTimeBeforeDrag = HitObject.EndTime;
|
||||||
|
double proposedStartTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos);
|
||||||
|
double proposedEndTime = endTimeBeforeDrag;
|
||||||
|
|
||||||
|
if (proposedStartTime >= proposedEndTime)
|
||||||
|
return;
|
||||||
|
|
||||||
|
HitObject.StartTime = proposedStartTime;
|
||||||
|
HitObject.EndTime = proposedEndTime;
|
||||||
|
editorBeatmap?.Update(HitObject);
|
||||||
|
},
|
||||||
|
DragEnded = () => changeHandler?.EndChange(),
|
||||||
|
},
|
||||||
|
tail = new EditHoldNoteEndPiece
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
DragStarted = () => changeHandler?.BeginChange(),
|
||||||
|
Dragging = pos =>
|
||||||
|
{
|
||||||
|
double proposedStartTime = HitObject.StartTime;
|
||||||
|
double proposedEndTime = positionSnapProvider?.FindSnappedPositionAndTime(pos).Time ?? HitObjectContainer.TimeAtScreenSpacePosition(pos);
|
||||||
|
|
||||||
|
if (proposedStartTime >= proposedEndTime)
|
||||||
|
return;
|
||||||
|
|
||||||
|
HitObject.StartTime = proposedStartTime;
|
||||||
|
HitObject.EndTime = proposedEndTime;
|
||||||
|
editorBeatmap?.Update(HitObject);
|
||||||
|
},
|
||||||
|
DragEnded = () => changeHandler?.EndChange(),
|
||||||
|
},
|
||||||
new Container
|
new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
|
public new ManiaPlayfield Playfield => drawableRuleset.Playfield;
|
||||||
|
|
||||||
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
|
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -16,6 +17,16 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private HitObjectComposer composer { get; set; } = null!;
|
private HitObjectComposer composer { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnSelectionChanged()
|
||||||
|
{
|
||||||
|
base.OnSelectionChanged();
|
||||||
|
|
||||||
|
var selectedObjects = SelectedItems.OfType<ManiaHitObject>().ToArray();
|
||||||
|
|
||||||
|
SelectionBox.CanFlipX = canFlipX(selectedObjects);
|
||||||
|
SelectionBox.CanFlipY = canFlipY(selectedObjects);
|
||||||
|
}
|
||||||
|
|
||||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||||
{
|
{
|
||||||
var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint;
|
var hitObjectBlueprint = (HitObjectSelectionBlueprint)moveEvent.Blueprint;
|
||||||
@ -26,6 +37,58 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
|
||||||
|
{
|
||||||
|
var selectedObjects = SelectedItems.OfType<ManiaHitObject>().ToArray();
|
||||||
|
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
|
||||||
|
|
||||||
|
if (selectedObjects.Length == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Direction.Horizontal:
|
||||||
|
if (!canFlipX(selectedObjects))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int firstColumn = flipOverOrigin ? 0 : selectedObjects.Min(ho => ho.Column);
|
||||||
|
int lastColumn = flipOverOrigin ? (int)EditorBeatmap.BeatmapInfo.Difficulty.CircleSize - 1 : selectedObjects.Max(ho => ho.Column);
|
||||||
|
|
||||||
|
EditorBeatmap.PerformOnSelection(hitObject =>
|
||||||
|
{
|
||||||
|
var maniaObject = (ManiaHitObject)hitObject;
|
||||||
|
maniaPlayfield.Remove(maniaObject);
|
||||||
|
maniaObject.Column = firstColumn + (lastColumn - maniaObject.Column);
|
||||||
|
maniaPlayfield.Add(maniaObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case Direction.Vertical:
|
||||||
|
if (!canFlipY(selectedObjects))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double selectionStartTime = selectedObjects.Min(ho => ho.StartTime);
|
||||||
|
double selectionEndTime = selectedObjects.Max(ho => ho.GetEndTime());
|
||||||
|
|
||||||
|
EditorBeatmap.PerformOnSelection(hitObject =>
|
||||||
|
{
|
||||||
|
hitObject.StartTime = selectionStartTime + (selectionEndTime - hitObject.GetEndTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(direction), direction, "Cannot flip over the supplied direction.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool canFlipX(ManiaHitObject[] selectedObjects)
|
||||||
|
=> selectedObjects.Select(ho => ho.Column).Distinct().Count() > 1;
|
||||||
|
|
||||||
|
private static bool canFlipY(ManiaHitObject[] selectedObjects)
|
||||||
|
=> selectedObjects.Length > 1 && selectedObjects.Min(ho => ho.StartTime) < selectedObjects.Max(ho => ho.GetEndTime());
|
||||||
|
|
||||||
private void performColumnMovement(int lastColumn, MoveSelectionEvent<HitObject> moveEvent)
|
private void performColumnMovement(int lastColumn, MoveSelectionEvent<HitObject> moveEvent)
|
||||||
{
|
{
|
||||||
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
|
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
|
||||||
|
@ -3,20 +3,155 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Setup;
|
using osu.Game.Screens.Edit.Setup;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Setup
|
namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||||
{
|
{
|
||||||
public partial class ManiaDifficultySection : DifficultySection
|
public partial class ManiaDifficultySection : SetupSection
|
||||||
{
|
{
|
||||||
|
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||||
|
|
||||||
|
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||||
|
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||||
|
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||||
|
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||||
|
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private Editor? editor { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania;
|
Children = new Drawable[]
|
||||||
CircleSizeSlider.Description = "The number of columns in the beatmap";
|
{
|
||||||
if (CircleSizeSlider.Current is BindableNumber<float> circleSizeFloat)
|
keyCountSlider = new LabelledSliderBar<float>
|
||||||
circleSizeFloat.Precision = 1;
|
{
|
||||||
|
Label = BeatmapsetsStrings.ShowStatsCsMania,
|
||||||
|
FixedLabelWidth = LABEL_WIDTH,
|
||||||
|
Description = "The number of columns in the beatmap",
|
||||||
|
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||||
|
{
|
||||||
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
healthDrainSlider = new LabelledSliderBar<float>
|
||||||
|
{
|
||||||
|
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||||
|
FixedLabelWidth = LABEL_WIDTH,
|
||||||
|
Description = EditorSetupStrings.DrainRateDescription,
|
||||||
|
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||||
|
{
|
||||||
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 0.1f,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overallDifficultySlider = new LabelledSliderBar<float>
|
||||||
|
{
|
||||||
|
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||||
|
FixedLabelWidth = LABEL_WIDTH,
|
||||||
|
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||||
|
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||||
|
{
|
||||||
|
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 0.1f,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
baseVelocitySlider = new LabelledSliderBar<double>
|
||||||
|
{
|
||||||
|
Label = EditorSetupStrings.BaseVelocity,
|
||||||
|
FixedLabelWidth = LABEL_WIDTH,
|
||||||
|
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||||
|
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||||
|
{
|
||||||
|
Default = 1.4,
|
||||||
|
MinValue = 0.4,
|
||||||
|
MaxValue = 3.6,
|
||||||
|
Precision = 0.01f,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tickRateSlider = new LabelledSliderBar<double>
|
||||||
|
{
|
||||||
|
Label = EditorSetupStrings.TickRate,
|
||||||
|
FixedLabelWidth = LABEL_WIDTH,
|
||||||
|
Description = EditorSetupStrings.TickRateDescription,
|
||||||
|
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||||
|
{
|
||||||
|
Default = 1,
|
||||||
|
MinValue = 1,
|
||||||
|
MaxValue = 4,
|
||||||
|
Precision = 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
keyCountSlider.Current.BindValueChanged(updateKeyCount);
|
||||||
|
healthDrainSlider.Current.BindValueChanged(_ => updateValues());
|
||||||
|
overallDifficultySlider.Current.BindValueChanged(_ => updateValues());
|
||||||
|
baseVelocitySlider.Current.BindValueChanged(_ => updateValues());
|
||||||
|
tickRateSlider.Current.BindValueChanged(_ => updateValues());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool updatingKeyCount;
|
||||||
|
|
||||||
|
private void updateKeyCount(ValueChangedEvent<float> keyCount)
|
||||||
|
{
|
||||||
|
if (updatingKeyCount) return;
|
||||||
|
|
||||||
|
updateValues();
|
||||||
|
|
||||||
|
if (editor == null) return;
|
||||||
|
|
||||||
|
updatingKeyCount = true;
|
||||||
|
|
||||||
|
editor.Reload().ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (!t.GetResultSafely())
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
changeHandler!.RestoreState(-1);
|
||||||
|
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value = keyCount.OldValue;
|
||||||
|
updatingKeyCount = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
updatingKeyCount = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateValues()
|
||||||
|
{
|
||||||
|
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||||
|
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||||
|
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||||
|
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||||
|
|
||||||
|
Beatmap.UpdateAllHitObjects();
|
||||||
|
Beatmap.SaveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,7 +421,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
||||||
|
|
||||||
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
||||||
|
|
||||||
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
||||||
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
|
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
|
||||||
|
@ -65,11 +65,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
|
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
|
||||||
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
|
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
|
||||||
|
|
||||||
light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d =>
|
light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength)?.With(d =>
|
||||||
{
|
{
|
||||||
if (d == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
d.Origin = Anchor.Centre;
|
d.Origin = Anchor.Centre;
|
||||||
d.Blending = BlendingParameters.Additive;
|
d.Blending = BlendingParameters.Additive;
|
||||||
d.Scale = new Vector2(lightScale);
|
d.Scale = new Vector2(lightScale);
|
||||||
@ -91,11 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
direction.BindTo(scrollingInfo.Direction);
|
direction.BindTo(scrollingInfo.Direction);
|
||||||
isHitting.BindTo(holdNote.IsHitting);
|
isHitting.BindTo(holdNote.IsHitting);
|
||||||
|
|
||||||
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d =>
|
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d =>
|
||||||
{
|
{
|
||||||
if (d == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (d is TextureAnimation animation)
|
if (d is TextureAnimation animation)
|
||||||
animation.IsPlaying = false;
|
animation.IsPlaying = false;
|
||||||
|
|
||||||
@ -140,10 +134,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
||||||
{
|
{
|
||||||
if (bodySprite is TextureAnimation bodyAnimation)
|
if (bodySprite is TextureAnimation bodyAnimation)
|
||||||
{
|
|
||||||
bodyAnimation.GotoFrame(0);
|
|
||||||
bodyAnimation.IsPlaying = isHitting.NewValue;
|
bodyAnimation.IsPlaying = isHitting.NewValue;
|
||||||
}
|
|
||||||
|
|
||||||
if (lightContainer == null)
|
if (lightContainer == null)
|
||||||
return;
|
return;
|
||||||
@ -219,6 +210,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
|
if (!isHitting.Value)
|
||||||
|
(bodySprite as TextureAnimation)?.GotoFrame(0);
|
||||||
|
|
||||||
if (holdNote.Body.HasHoldBreak)
|
if (holdNote.Body.HasHoldBreak)
|
||||||
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
|
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
|
||||||
|
|
||||||
@ -245,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
// i dunno this looks about right??
|
// i dunno this looks about right??
|
||||||
// the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
|
// the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
|
||||||
if (sprite.DrawHeight > 0)
|
if (sprite.DrawHeight > 0)
|
||||||
bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
|
bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -43,11 +43,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
|
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
|
||||||
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
|
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
|
||||||
|
|
||||||
explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d =>
|
explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength)?.With(d =>
|
||||||
{
|
{
|
||||||
if (d == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
d.Origin = Anchor.Centre;
|
d.Origin = Anchor.Centre;
|
||||||
d.Blending = BlendingParameters.Additive;
|
d.Blending = BlendingParameters.Additive;
|
||||||
d.Scale = new Vector2(explosionScale);
|
d.Scale = new Vector2(explosionScale);
|
||||||
|
@ -28,13 +28,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
string bottomImage = skin.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
|
string bottomImage = skin.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value
|
||||||
?? "mania-stage-bottom";
|
?? "mania-stage-bottom";
|
||||||
|
|
||||||
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d =>
|
sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => d.Scale = new Vector2(1.6f));
|
||||||
{
|
|
||||||
if (d == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
d.Scale = new Vector2(1.6f);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sprite != null)
|
if (sprite != null)
|
||||||
InternalChild = sprite;
|
InternalChild = sprite;
|
||||||
|
@ -93,8 +93,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
|
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
|
||||||
// (see `Stage.columnBackgrounds`).
|
// (see `Stage.columnBackgrounds`).
|
||||||
BackgroundContainer,
|
BackgroundContainer,
|
||||||
TopLevelContainer,
|
TopLevelContainer
|
||||||
new ColumnTouchInputArea(this)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
|
||||||
@ -181,38 +180,5 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||||
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
||||||
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||||
|
|
||||||
public partial class ColumnTouchInputArea : Drawable
|
|
||||||
{
|
|
||||||
private readonly Column column;
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
|
||||||
private ManiaInputManager maniaInputManager { get; set; }
|
|
||||||
|
|
||||||
private KeyBindingContainer<ManiaAction> keyBindingContainer;
|
|
||||||
|
|
||||||
public ColumnTouchInputArea(Column column)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
this.column = column;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnTouchDown(TouchDownEvent e)
|
|
||||||
{
|
|
||||||
keyBindingContainer?.TriggerPressed(column.Action.Value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTouchUp(TouchUpEvent e)
|
|
||||||
{
|
|
||||||
keyBindingContainer?.TriggerReleased(column.Action.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,12 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Framework;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Skinning;
|
using osu.Game.Rulesets.Mania.Skinning;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
@ -62,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
onSkinChanged();
|
onSkinChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
updateMobileSizing();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSkinChanged()
|
private void onSkinChanged()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||||
@ -92,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
columns[i].Width = width.Value;
|
columns[i].Width = width.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMobileSizing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -106,31 +95,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
Content[column] = columns[column].Child = content;
|
Content[column] = columns[column].Child = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMobileSizing()
|
|
||||||
{
|
|
||||||
if (!IsLoaded || !RuntimeInfo.IsMobile)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
|
|
||||||
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
|
|
||||||
|
|
||||||
// Will be null in tests.
|
|
||||||
if (containingCell == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
|
|
||||||
|
|
||||||
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
|
|
||||||
float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
|
|
||||||
// 1.92 is a "reference" mobile screen aspect ratio for phones.
|
|
||||||
// We should scale it back for cases like tablets which aren't so extreme.
|
|
||||||
mobileAdjust *= aspectRatio / 1.92f;
|
|
||||||
|
|
||||||
// Best effort until we have better mobile support.
|
|
||||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
|
||||||
columns[i].Width *= mobileAdjust;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -31,6 +31,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
|
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const double MAX_TIME_RANGE = 11485;
|
public const double MAX_TIME_RANGE = 11485;
|
||||||
|
|
||||||
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
public new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
|
||||||
|
|
||||||
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
|
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
|
||||||
|
|
||||||
@ -103,6 +104,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
|
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
|
||||||
|
|
||||||
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||||
|
|
||||||
|
KeyBindingInputManager.Add(new ManiaTouchInputArea());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
||||||
|
216
osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs
Normal file
216
osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An overlay that captures and displays osu!mania mouse and touch input.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ManiaTouchInputArea : VisibilityContainer
|
||||||
|
{
|
||||||
|
// visibility state affects our child. we always want to handle input.
|
||||||
|
public override bool PropagatePositionalInputSubTree => true;
|
||||||
|
public override bool PropagateNonPositionalInputSubTree => true;
|
||||||
|
|
||||||
|
[SettingSource("Spacing", "The spacing between receptors.")]
|
||||||
|
public BindableFloat Spacing { get; } = new BindableFloat(10)
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
[SettingSource("Opacity", "The receptor opacity.")]
|
||||||
|
public BindableFloat Opacity { get; } = new BindableFloat(1)
|
||||||
|
{
|
||||||
|
Precision = 0.1f,
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
|
||||||
|
|
||||||
|
private GridContainer gridContainer = null!;
|
||||||
|
|
||||||
|
public ManiaTouchInputArea()
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomCentre;
|
||||||
|
Origin = Anchor.BottomCentre;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Height = 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
List<Drawable> receptorGridContent = new List<Drawable>();
|
||||||
|
List<Dimension> receptorGridDimensions = new List<Dimension>();
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
foreach (var stage in drawableRuleset.Playfield.Stages)
|
||||||
|
{
|
||||||
|
foreach (var column in stage.Columns)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
|
||||||
|
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
|
||||||
|
receptorGridDimensions.Add(new Dimension());
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalChild = gridContainer = new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Content = new[] { receptorGridContent.ToArray() },
|
||||||
|
ColumnDimensions = receptorGridDimensions.ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
// Hide whenever the keyboard is used.
|
||||||
|
Hide();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
|
{
|
||||||
|
Show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
gridContainer.FadeIn(500, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
gridContainer.FadeOut(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ColumnInputReceptor : CompositeDrawable
|
||||||
|
{
|
||||||
|
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||||
|
|
||||||
|
private readonly Box highlightOverlay;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ManiaInputManager? inputManager { get; set; }
|
||||||
|
|
||||||
|
private bool isPressed;
|
||||||
|
|
||||||
|
public ColumnInputReceptor()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 10,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0.15f,
|
||||||
|
},
|
||||||
|
highlightOverlay = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
|
{
|
||||||
|
updateButton(true);
|
||||||
|
return false; // handled by parent container to show overlay.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnTouchUp(TouchUpEvent e)
|
||||||
|
{
|
||||||
|
updateButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
updateButton(true);
|
||||||
|
return false; // handled by parent container to show overlay.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
|
{
|
||||||
|
updateButton(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButton(bool press)
|
||||||
|
{
|
||||||
|
if (press == isPressed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
isPressed = press;
|
||||||
|
|
||||||
|
if (press)
|
||||||
|
{
|
||||||
|
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value);
|
||||||
|
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value);
|
||||||
|
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class Gutter : Drawable
|
||||||
|
{
|
||||||
|
public readonly IBindable<float> Spacing = new Bindable<float>();
|
||||||
|
|
||||||
|
public Gutter()
|
||||||
|
{
|
||||||
|
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,9 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit;
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -25,22 +27,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
|
||||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
rectangularGridActive(false);
|
gridActive<RectangularPositionSnapGrid>(false);
|
||||||
|
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
|
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
rectangularGridActive(true);
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
|
||||||
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
|
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
|
||||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
rectangularGridActive(true);
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
|
||||||
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
|
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
rectangularGridActive(false);
|
gridActive<RectangularPositionSnapGrid>(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -117,33 +119,56 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestGridSnapMomentaryToggle()
|
public void TestGridSnapMomentaryToggle()
|
||||||
{
|
{
|
||||||
rectangularGridActive(false);
|
gridActive<RectangularPositionSnapGrid>(false);
|
||||||
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||||
rectangularGridActive(true);
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||||
rectangularGridActive(false);
|
gridActive<RectangularPositionSnapGrid>(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void rectangularGridActive(bool active)
|
private void gridActive<T>(bool active) where T : PositionSnapGrid
|
||||||
{
|
{
|
||||||
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
|
||||||
AddStep("move cursor to (1, 1)", () =>
|
AddStep("move cursor to spacing + (1, 1)", () =>
|
||||||
{
|
{
|
||||||
var composer = Editor.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single();
|
var composer = Editor.ChildrenOfType<T>().Single();
|
||||||
InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1)));
|
InputManager.MoveMouseTo(composer.ToScreenSpace(uniqueSnappingPosition(composer) + new Vector2(1, 1)));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (active)
|
if (active)
|
||||||
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
|
{
|
||||||
|
AddAssert("placement blueprint at spacing + (0, 0)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<T>().Single();
|
||||||
|
return Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position,
|
||||||
|
uniqueSnappingPosition(composer));
|
||||||
|
});
|
||||||
|
}
|
||||||
else
|
else
|
||||||
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
|
{
|
||||||
|
AddAssert("placement blueprint at spacing + (1, 1)", () =>
|
||||||
|
{
|
||||||
|
var composer = Editor.ChildrenOfType<T>().Single();
|
||||||
|
return Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position,
|
||||||
|
uniqueSnappingPosition(composer) + new Vector2(1, 1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 uniqueSnappingPosition(PositionSnapGrid grid)
|
||||||
|
{
|
||||||
|
return grid switch
|
||||||
|
{
|
||||||
|
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
|
||||||
|
_ => Vector2.Zero
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestGridSizeToggling()
|
public void TestGridSizeToggling()
|
||||||
{
|
{
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Any());
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
gridSizeIs(4);
|
gridSizeIs(4);
|
||||||
|
|
||||||
nextGridSizeIs(8);
|
nextGridSizeIs(8);
|
||||||
@ -159,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void gridSizeIs(int size)
|
private void gridSizeIs(int size)
|
||||||
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single().Spacing == new Vector2(size)
|
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
||||||
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,12 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -71,4 +74,120 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
private void moveMouse(Vector2 pos) =>
|
private void moveMouse(Vector2 pos) =>
|
||||||
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
|
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSliderNearLinearScaling
|
||||||
|
{
|
||||||
|
private readonly Random rng = new Random(1337);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScalingSliderFlat()
|
||||||
|
{
|
||||||
|
SliderPath sliderPathPerfect = new SliderPath(
|
||||||
|
[
|
||||||
|
new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE),
|
||||||
|
new PathControlPoint(new Vector2(50, 25)),
|
||||||
|
new PathControlPoint(new Vector2(25, 100)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
SliderPath sliderPathBezier = new SliderPath(
|
||||||
|
[
|
||||||
|
new PathControlPoint(new Vector2(0), PathType.BEZIER),
|
||||||
|
new PathControlPoint(new Vector2(50, 25)),
|
||||||
|
new PathControlPoint(new Vector2(25, 100)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
scaleSlider(sliderPathPerfect, new Vector2(0.000001f, 1));
|
||||||
|
scaleSlider(sliderPathBezier, new Vector2(0.000001f, 1));
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
Assert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPerfectCurveMatchesTheoretical()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 20000; i++)
|
||||||
|
{
|
||||||
|
//Only test points that are in the screen's bounds
|
||||||
|
float p1X = 640.0f * (float)rng.NextDouble();
|
||||||
|
float p2X = 640.0f * (float)rng.NextDouble();
|
||||||
|
|
||||||
|
float p1Y = 480.0f * (float)rng.NextDouble();
|
||||||
|
float p2Y = 480.0f * (float)rng.NextDouble();
|
||||||
|
SliderPath sliderPathPerfect = new SliderPath(
|
||||||
|
[
|
||||||
|
new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE),
|
||||||
|
new PathControlPoint(new Vector2(p1X, p1Y)),
|
||||||
|
new PathControlPoint(new Vector2(p2X, p2Y)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertMatchesPerfectCircle(sliderPathPerfect);
|
||||||
|
|
||||||
|
scaleSlider(sliderPathPerfect, new Vector2(0.00001f, 1));
|
||||||
|
|
||||||
|
assertMatchesPerfectCircle(sliderPathPerfect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMatchesPerfectCircle(SliderPath path)
|
||||||
|
{
|
||||||
|
if (path.ControlPoints.Count != 3)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Replication of PathApproximator.CircularArcToPiecewiseLinear
|
||||||
|
CircularArcProperties circularArcProperties = new CircularArcProperties(path.ControlPoints.Select(x => x.Position).ToArray());
|
||||||
|
|
||||||
|
if (!circularArcProperties.IsValid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Addresses cases where circularArcProperties.ThetaRange>0.5
|
||||||
|
//Occurs in code in PathControlPointVisualiser.ensureValidPathType
|
||||||
|
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(path.ControlPoints.Select(x => x.Position).ToArray());
|
||||||
|
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int subpoints = (2f * circularArcProperties.Radius <= 0.1f) ? 2 : Math.Max(2, (int)Math.Ceiling(circularArcProperties.ThetaRange / (2.0 * Math.Acos(1f - (0.1f / circularArcProperties.Radius)))));
|
||||||
|
|
||||||
|
//ignore cases where subpoints is int.MaxValue, result will be garbage
|
||||||
|
//as well, having this many subpoints will cause an out of memory error, so can't happen during normal useage
|
||||||
|
if (subpoints == int.MaxValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < Math.Min(subpoints, 100); i++)
|
||||||
|
{
|
||||||
|
float progress = (float)rng.NextDouble();
|
||||||
|
|
||||||
|
//To avoid errors from interpolating points, ensure we check only positions that would be subpoints.
|
||||||
|
progress = (float)Math.Ceiling(progress * (subpoints - 1)) / (subpoints - 1);
|
||||||
|
|
||||||
|
//Special case - if few subpoints, ensure checking every single one rather than randomly
|
||||||
|
if (subpoints < 100)
|
||||||
|
progress = i / (float)(subpoints - 1);
|
||||||
|
|
||||||
|
//edge points cause issue with interpolation, so ignore the last two points and first
|
||||||
|
if (progress == 0.0f || progress >= (subpoints - 2) / (float)(subpoints - 1))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
double theta = circularArcProperties.ThetaStart + (circularArcProperties.Direction * progress * circularArcProperties.ThetaRange);
|
||||||
|
Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius;
|
||||||
|
|
||||||
|
Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f),
|
||||||
|
"A perfect circle with points " + string.Join(", ", path.ControlPoints.Select(x => x.Position)) + " and radius" + circularArcProperties.Radius + "from SliderPath does not almost equal a theoretical perfect circle with " + subpoints + " subpoints"
|
||||||
|
+ ": " + (circularArcProperties.Centre + vector) + " - " + path.PositionAt(progress)
|
||||||
|
+ " = " + (circularArcProperties.Centre + vector - path.PositionAt(progress))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scaleSlider(SliderPath path, Vector2 scale)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < path.ControlPoints.Count; i++)
|
||||||
|
{
|
||||||
|
path.ControlPoints[i].Position *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
Autoplay = false,
|
Autoplay = false,
|
||||||
Beatmap = new Beatmap
|
Beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
Breaks = new List<BreakPeriod>
|
Breaks =
|
||||||
{
|
{
|
||||||
new BreakPeriod(500, 2000),
|
new BreakPeriod(500, 2000),
|
||||||
},
|
},
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
StartTime = 5000,
|
StartTime = 5000,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Breaks = new List<BreakPeriod>
|
Breaks =
|
||||||
{
|
{
|
||||||
new BreakPeriod(2000, 4000),
|
new BreakPeriod(2000, 4000),
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
Autoplay = false,
|
Autoplay = false,
|
||||||
Beatmap = new Beatmap
|
Beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
Breaks = new List<BreakPeriod>
|
Breaks =
|
||||||
{
|
{
|
||||||
new BreakPeriod(500, 2000),
|
new BreakPeriod(500, 2000),
|
||||||
},
|
},
|
||||||
|
@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private partial class TestDrawableOsuJudgement : DrawableOsuJudgement
|
private partial class TestDrawableOsuJudgement : DrawableOsuJudgement
|
||||||
{
|
{
|
||||||
public new SkinnableSprite Lighting => base.Lighting;
|
public new SkinnableSprite Lighting => base.Lighting;
|
||||||
public new SkinnableDrawable JudgementBody => base.JudgementBody;
|
public new SkinnableDrawable? JudgementBody => base.JudgementBody;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,9 +161,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
pressed = value;
|
pressed = value;
|
||||||
if (value)
|
if (value)
|
||||||
OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
|
OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton));
|
||||||
else
|
else
|
||||||
OnReleased(new KeyBindingReleaseEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
|
OnReleased(new KeyBindingReleaseEvent<OsuAction>(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private void scheduleHit() => AddStep("schedule action", () =>
|
private void scheduleHit() => AddStep("schedule action", () =>
|
||||||
{
|
{
|
||||||
double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
|
double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
|
||||||
Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton)), delay);
|
Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager()!.CurrentState, OsuAction.LeftButton)), delay);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ using osu.Game.Beatmaps.ControlPoints;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -578,6 +579,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
assertKeyCounter(1, 1);
|
assertKeyCounter(1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTouchJudgedCircle()
|
||||||
|
{
|
||||||
|
addHitCircleAt(TouchSource.Touch1);
|
||||||
|
addHitCircleAt(TouchSource.Touch2);
|
||||||
|
|
||||||
|
beginTouch(TouchSource.Touch1);
|
||||||
|
endTouch(TouchSource.Touch1);
|
||||||
|
|
||||||
|
// Hold the second touch (this becomes the primary touch).
|
||||||
|
beginTouch(TouchSource.Touch2);
|
||||||
|
|
||||||
|
// Touch again on the first circle.
|
||||||
|
// Because it's been judged, the cursor should not move here.
|
||||||
|
beginTouch(TouchSource.Touch1);
|
||||||
|
checkPosition(TouchSource.Touch2);
|
||||||
|
}
|
||||||
|
|
||||||
private void addHitCircleAt(TouchSource source)
|
private void addHitCircleAt(TouchSource source)
|
||||||
{
|
{
|
||||||
AddStep($"Add circle at {source}", () =>
|
AddStep($"Add circle at {source}", () =>
|
||||||
@ -590,6 +609,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
Clock = new FramedClock(new ManualClock()),
|
Clock = new FramedClock(new ManualClock()),
|
||||||
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
|
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
|
||||||
|
CheckHittable = (_, _, _) => ClickAction.Hit
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -156,6 +156,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
slider = (DrawableSlider)createSlider(repeats: 1);
|
slider = (DrawableSlider)createSlider(repeats: 1);
|
||||||
Add(slider);
|
Add(slider);
|
||||||
|
slider.HitObject.NodeSamples.Clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("change samples", () => slider.HitObject.Samples = new[]
|
AddStep("change samples", () => slider.HitObject.Samples = new[]
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -41,22 +42,22 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
{
|
{
|
||||||
base.PostProcess();
|
base.PostProcess();
|
||||||
|
|
||||||
var osuBeatmap = (Beatmap<OsuHitObject>)Beatmap;
|
var hitObjects = Beatmap.HitObjects as List<OsuHitObject> ?? Beatmap.HitObjects.OfType<OsuHitObject>().ToList();
|
||||||
|
|
||||||
if (osuBeatmap.HitObjects.Count > 0)
|
if (hitObjects.Count > 0)
|
||||||
{
|
{
|
||||||
// Reset stacking
|
// Reset stacking
|
||||||
foreach (var h in osuBeatmap.HitObjects)
|
foreach (var h in hitObjects)
|
||||||
h.StackHeight = 0;
|
h.StackHeight = 0;
|
||||||
|
|
||||||
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
|
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
|
||||||
applyStacking(osuBeatmap, 0, osuBeatmap.HitObjects.Count - 1);
|
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
|
||||||
else
|
else
|
||||||
applyStackingOld(osuBeatmap);
|
applyStackingOld(Beatmap.BeatmapInfo, hitObjects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex)
|
private void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
||||||
@ -64,24 +65,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
|
|
||||||
int extendedEndIndex = endIndex;
|
int extendedEndIndex = endIndex;
|
||||||
|
|
||||||
if (endIndex < beatmap.HitObjects.Count - 1)
|
if (endIndex < hitObjects.Count - 1)
|
||||||
{
|
{
|
||||||
// Extend the end index to include objects they are stacked on
|
// Extend the end index to include objects they are stacked on
|
||||||
for (int i = endIndex; i >= startIndex; i--)
|
for (int i = endIndex; i >= startIndex; i--)
|
||||||
{
|
{
|
||||||
int stackBaseIndex = i;
|
int stackBaseIndex = i;
|
||||||
|
|
||||||
for (int n = stackBaseIndex + 1; n < beatmap.HitObjects.Count; n++)
|
for (int n = stackBaseIndex + 1; n < hitObjects.Count; n++)
|
||||||
{
|
{
|
||||||
OsuHitObject stackBaseObject = beatmap.HitObjects[stackBaseIndex];
|
OsuHitObject stackBaseObject = hitObjects[stackBaseIndex];
|
||||||
if (stackBaseObject is Spinner) break;
|
if (stackBaseObject is Spinner) break;
|
||||||
|
|
||||||
OsuHitObject objectN = beatmap.HitObjects[n];
|
OsuHitObject objectN = hitObjects[n];
|
||||||
if (objectN is Spinner)
|
if (objectN is Spinner)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
double endTime = stackBaseObject.GetEndTime();
|
double endTime = stackBaseObject.GetEndTime();
|
||||||
double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo.StackLeniency;
|
double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency;
|
||||||
|
|
||||||
if (objectN.StartTime - endTime > stackThreshold)
|
if (objectN.StartTime - endTime > stackThreshold)
|
||||||
// We are no longer within stacking range of the next object.
|
// We are no longer within stacking range of the next object.
|
||||||
@ -100,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
if (stackBaseIndex > extendedEndIndex)
|
if (stackBaseIndex > extendedEndIndex)
|
||||||
{
|
{
|
||||||
extendedEndIndex = stackBaseIndex;
|
extendedEndIndex = stackBaseIndex;
|
||||||
if (extendedEndIndex == beatmap.HitObjects.Count - 1)
|
if (extendedEndIndex == hitObjects.Count - 1)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,10 +124,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
* 2 and 1 will be ignored in the i loop because they already have a stack value.
|
* 2 and 1 will be ignored in the i loop because they already have a stack value.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
OsuHitObject objectI = beatmap.HitObjects[i];
|
OsuHitObject objectI = hitObjects[i];
|
||||||
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
|
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
|
||||||
|
|
||||||
double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo.StackLeniency;
|
double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency;
|
||||||
|
|
||||||
/* If this object is a hitcircle, then we enter this "special" case.
|
/* If this object is a hitcircle, then we enter this "special" case.
|
||||||
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
|
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
|
||||||
@ -136,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
{
|
{
|
||||||
while (--n >= 0)
|
while (--n >= 0)
|
||||||
{
|
{
|
||||||
OsuHitObject objectN = beatmap.HitObjects[n];
|
OsuHitObject objectN = hitObjects[n];
|
||||||
if (objectN is Spinner) continue;
|
if (objectN is Spinner) continue;
|
||||||
|
|
||||||
double endTime = objectN.GetEndTime();
|
double endTime = objectN.GetEndTime();
|
||||||
@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
for (int j = n + 1; j <= i; j++)
|
for (int j = n + 1; j <= i; j++)
|
||||||
{
|
{
|
||||||
// For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
|
// For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
|
||||||
OsuHitObject objectJ = beatmap.HitObjects[j];
|
OsuHitObject objectJ = hitObjects[j];
|
||||||
if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance)
|
if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance)
|
||||||
objectJ.StackHeight -= offset;
|
objectJ.StackHeight -= offset;
|
||||||
}
|
}
|
||||||
@ -191,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
*/
|
*/
|
||||||
while (--n >= startIndex)
|
while (--n >= startIndex)
|
||||||
{
|
{
|
||||||
OsuHitObject objectN = beatmap.HitObjects[n];
|
OsuHitObject objectN = hitObjects[n];
|
||||||
if (objectN is Spinner) continue;
|
if (objectN is Spinner) continue;
|
||||||
|
|
||||||
if (objectI.StartTime - objectN.StartTime > stackThreshold)
|
if (objectI.StartTime - objectN.StartTime > stackThreshold)
|
||||||
@ -208,11 +209,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStackingOld(Beatmap<OsuHitObject> beatmap)
|
private void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < beatmap.HitObjects.Count; i++)
|
for (int i = 0; i < hitObjects.Count; i++)
|
||||||
{
|
{
|
||||||
OsuHitObject currHitObject = beatmap.HitObjects[i];
|
OsuHitObject currHitObject = hitObjects[i];
|
||||||
|
|
||||||
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
|
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
|
||||||
continue;
|
continue;
|
||||||
@ -220,11 +221,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
double startTime = currHitObject.GetEndTime();
|
double startTime = currHitObject.GetEndTime();
|
||||||
int sliderStack = 0;
|
int sliderStack = 0;
|
||||||
|
|
||||||
for (int j = i + 1; j < beatmap.HitObjects.Count; j++)
|
for (int j = i + 1; j < hitObjects.Count; j++)
|
||||||
{
|
{
|
||||||
double stackThreshold = beatmap.HitObjects[i].TimePreempt * beatmap.BeatmapInfo.StackLeniency;
|
double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency;
|
||||||
|
|
||||||
if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime)
|
if (hitObjects[j].StartTime - stackThreshold > startTime)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// The start position of the hitobject, or the position at the end of the path if the hitobject is a slider
|
// The start position of the hitobject, or the position at the end of the path if the hitobject is a slider
|
||||||
@ -239,17 +240,17 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
||||||
// if we use `EndTime` here it would result in unexpected stacking.
|
// if we use `EndTime` here it would result in unexpected stacking.
|
||||||
|
|
||||||
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
|
if (Vector2Extensions.Distance(hitObjects[j].Position, currHitObject.Position) < stack_distance)
|
||||||
{
|
{
|
||||||
currHitObject.StackHeight++;
|
currHitObject.StackHeight++;
|
||||||
startTime = beatmap.HitObjects[j].StartTime;
|
startTime = hitObjects[j].StartTime;
|
||||||
}
|
}
|
||||||
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
|
else if (Vector2Extensions.Distance(hitObjects[j].Position, position2) < stack_distance)
|
||||||
{
|
{
|
||||||
// Case for sliders - bump notes down and right, rather than up and left.
|
// Case for sliders - bump notes down and right, rather than up and left.
|
||||||
sliderStack++;
|
sliderStack++;
|
||||||
beatmap.HitObjects[j].StackHeight -= sliderStack;
|
hitObjects[j].StackHeight -= sliderStack;
|
||||||
startTime = beatmap.HitObjects[j].StartTime;
|
startTime = hitObjects[j].StartTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
|
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
|
||||||
rhythmStart++;
|
rhythmStart++;
|
||||||
|
|
||||||
|
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
|
||||||
|
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
|
||||||
|
|
||||||
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);
|
||||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i);
|
|
||||||
OsuDifficultyHitObject lastObj = (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
|
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
||||||
|
|
||||||
@ -66,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
||||||
effectiveRatio *= 0.125;
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
effectiveRatio *= 0.25;
|
effectiveRatio *= 0.25;
|
||||||
|
|
||||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
||||||
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
startRatio = effectiveRatio;
|
startRatio = effectiveRatio;
|
||||||
islandSize = 1;
|
islandSize = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastObj = prevObj;
|
||||||
|
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_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
|
@ -7,7 +7,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -16,7 +15,6 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
|
|||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
||||||
{
|
{
|
||||||
@ -48,13 +46,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Circle
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = Color4.White,
|
|
||||||
},
|
|
||||||
ring = new RingPiece
|
ring = new RingPiece
|
||||||
{
|
{
|
||||||
BorderThickness = 4,
|
BorderThickness = 4,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
@ -16,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
|||||||
|
|
||||||
protected readonly HitCirclePiece CirclePiece;
|
protected readonly HitCirclePiece CirclePiece;
|
||||||
private readonly HitCircleOverlapMarker marker;
|
private readonly HitCircleOverlapMarker marker;
|
||||||
|
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
|
||||||
|
|
||||||
public HitCircleSelectionBlueprint(HitCircle circle)
|
public HitCircleSelectionBlueprint(HitCircle circle)
|
||||||
: base(circle)
|
: base(circle)
|
||||||
@ -27,12 +31,32 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuConfigManager config)
|
||||||
|
{
|
||||||
|
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
showHitMarkers.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (!showHitMarkers.Value)
|
||||||
|
DrawableObject.RestoreHitAnimations();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
CirclePiece.UpdateFrom(HitObject);
|
CirclePiece.UpdateFrom(HitObject);
|
||||||
marker.UpdateFrom(HitObject);
|
marker.UpdateFrom(HitObject);
|
||||||
|
|
||||||
|
if (showHitMarkers.Value)
|
||||||
|
DrawableObject.SuppressHitAnimations();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
@ -8,9 +8,9 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public readonly PathControlPoint ControlPoint;
|
public readonly PathControlPoint ControlPoint;
|
||||||
|
|
||||||
private readonly T hitObject;
|
private readonly T hitObject;
|
||||||
private readonly Container marker;
|
private readonly Circle circle;
|
||||||
private readonly Drawable markerRing;
|
private readonly Drawable markerRing;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -60,38 +60,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
marker = new Container
|
circle = new Circle
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
AutoSizeAxes = Axes.Both,
|
Size = new Vector2(20),
|
||||||
Children = new[]
|
},
|
||||||
{
|
markerRing = new CircularProgress
|
||||||
new Circle
|
{
|
||||||
{
|
Anchor = Anchor.Centre,
|
||||||
Anchor = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Size = new Vector2(28),
|
||||||
Size = new Vector2(20),
|
Alpha = 0,
|
||||||
},
|
InnerRadius = 0.1f,
|
||||||
markerRing = new CircularContainer
|
Progress = 1
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(28),
|
|
||||||
Masking = true,
|
|
||||||
BorderThickness = 2,
|
|
||||||
BorderColour = Color4.White,
|
|
||||||
Alpha = 0,
|
|
||||||
Child = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Alpha = 0,
|
|
||||||
AlwaysPresent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -115,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The connecting path is excluded from positional input
|
// The connecting path is excluded from positional input
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
@ -209,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (IsHovered || IsSelected.Value)
|
if (IsHovered || IsSelected.Value)
|
||||||
colour = colour.Lighten(1);
|
colour = colour.Lighten(1);
|
||||||
|
|
||||||
marker.Colour = colour;
|
Colour = colour;
|
||||||
marker.Scale = new Vector2(hitObject.Scale);
|
Scale = new Vector2(hitObject.Scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color4 getColourFromNodeType()
|
private Color4 getColourFromNodeType()
|
||||||
|
@ -435,10 +435,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
|
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
|
||||||
{
|
{
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||||
updatePathType(p, type);
|
updatePathType(p, type);
|
||||||
|
|
||||||
EnsureValidPathTypes();
|
EnsureValidPathTypes();
|
||||||
|
|
||||||
|
changeHandler?.EndChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (countOfState == totalCount)
|
if (countOfState == totalCount)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
@ -27,14 +28,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
public SliderBodyPiece()
|
public SliderBodyPiece()
|
||||||
{
|
{
|
||||||
InternalChild = body = new ManualSliderBody
|
AutoSizeAxes = Axes.Both;
|
||||||
{
|
|
||||||
AccentColour = Color4.Transparent
|
|
||||||
};
|
|
||||||
|
|
||||||
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
|
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
|
||||||
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
|
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
|
||||||
AlwaysPresent = true;
|
AlwaysPresent = true;
|
||||||
|
|
||||||
|
InternalChild = body = new ManualSliderBody
|
||||||
|
{
|
||||||
|
AccentColour = Color4.Transparent
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -61,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
body.SetVertices(vertices);
|
body.SetVertices(vertices);
|
||||||
}
|
}
|
||||||
|
|
||||||
Size = body.Size;
|
|
||||||
OriginPosition = body.PathOffset;
|
OriginPosition = body.PathOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -14,18 +13,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private readonly Slider slider;
|
private readonly Slider slider;
|
||||||
private readonly SliderPosition position;
|
private readonly SliderPosition position;
|
||||||
private readonly HitCircleOverlapMarker marker;
|
private readonly HitCircleOverlapMarker? marker;
|
||||||
|
|
||||||
public SliderCircleOverlay(Slider slider, SliderPosition position)
|
public SliderCircleOverlay(Slider slider, SliderPosition position)
|
||||||
{
|
{
|
||||||
this.slider = slider;
|
this.slider = slider;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
if (position == SliderPosition.Start)
|
||||||
{
|
AddInternal(marker = new HitCircleOverlapMarker());
|
||||||
marker = new HitCircleOverlapMarker(),
|
|
||||||
CirclePiece = new HitCirclePiece(),
|
AddInternal(CirclePiece = new HitCirclePiece());
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -35,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
|
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
|
||||||
|
|
||||||
CirclePiece.UpdateFrom(circle);
|
CirclePiece.UpdateFrom(circle);
|
||||||
marker.UpdateFrom(circle);
|
marker?.UpdateFrom(circle);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Hide()
|
public override void Hide()
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
|
|||||||
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;
|
||||||
|
using osu.Game.Configuration;
|
||||||
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;
|
||||||
@ -59,6 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
||||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||||
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
|
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
|
||||||
|
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
|
||||||
|
|
||||||
public SliderSelectionBlueprint(Slider slider)
|
public SliderSelectionBlueprint(Slider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
@ -66,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -74,6 +76,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start),
|
HeadOverlay = CreateCircleOverlay(HitObject, SliderPosition.Start),
|
||||||
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
|
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -90,6 +94,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
if (editorBeatmap != null)
|
if (editorBeatmap != null)
|
||||||
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true);
|
selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true);
|
||||||
|
showHitMarkers.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
if (!showHitMarkers.Value)
|
||||||
|
DrawableObject.RestoreHitAnimations();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HandleQuickDeletion()
|
public override bool HandleQuickDeletion()
|
||||||
@ -110,6 +119,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
if (IsSelected)
|
if (IsSelected)
|
||||||
BodyPiece.UpdateFrom(HitObject);
|
BodyPiece.UpdateFrom(HitObject);
|
||||||
|
|
||||||
|
if (showHitMarkers.Value)
|
||||||
|
DrawableObject.SuppressHitAnimations();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
@ -23,12 +26,32 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private partial class OsuEditorPlayfield : OsuPlayfield
|
private partial class OsuEditorPlayfield : OsuPlayfield
|
||||||
{
|
{
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
protected override GameplayCursorContainer? CreateCursor() => null;
|
protected override GameplayCursorContainer? CreateCursor() => null;
|
||||||
|
|
||||||
public OsuEditorPlayfield()
|
public OsuEditorPlayfield()
|
||||||
{
|
{
|
||||||
HitPolicy = new AnyOrderHitPolicy();
|
HitPolicy = new AnyOrderHitPolicy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
editorBeatmap.BeatmapReprocessed += onBeatmapReprocessed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onBeatmapReprocessed() => ApplyCircleSizeToPlayfieldBorder(editorBeatmap);
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (editorBeatmap.IsNotNull())
|
||||||
|
editorBeatmap.BeatmapReprocessed -= onBeatmapReprocessed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
171
osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs
Normal file
171
osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// X position of the grid's origin.
|
||||||
|
/// </summary>
|
||||||
|
public BindableFloat StartPositionX { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.X / 2)
|
||||||
|
{
|
||||||
|
MinValue = 0f,
|
||||||
|
MaxValue = OsuPlayfield.BASE_SIZE.X,
|
||||||
|
Precision = 1f
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Y position of the grid's origin.
|
||||||
|
/// </summary>
|
||||||
|
public BindableFloat StartPositionY { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.Y / 2)
|
||||||
|
{
|
||||||
|
MinValue = 0f,
|
||||||
|
MaxValue = OsuPlayfield.BASE_SIZE.Y,
|
||||||
|
Precision = 1f
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The spacing between grid lines.
|
||||||
|
/// </summary>
|
||||||
|
public BindableFloat Spacing { get; } = new BindableFloat(4f)
|
||||||
|
{
|
||||||
|
MinValue = 4f,
|
||||||
|
MaxValue = 128f,
|
||||||
|
Precision = 1f
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rotation of the grid lines in degrees.
|
||||||
|
/// </summary>
|
||||||
|
public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f)
|
||||||
|
{
|
||||||
|
MinValue = -45f,
|
||||||
|
MaxValue = 45f,
|
||||||
|
Precision = 1f
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only bindable representing the grid's origin.
|
||||||
|
/// Equivalent to <code>new Vector2(StartPositionX, StartPositionY)</code>
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only bindable representing the grid's spacing in both the X and Y dimension.
|
||||||
|
/// Equivalent to <code>new Vector2(Spacing)</code>
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<Vector2> SpacingVector { get; } = new Bindable<Vector2>();
|
||||||
|
|
||||||
|
private ExpandableSlider<float> startPositionXSlider = null!;
|
||||||
|
private ExpandableSlider<float> startPositionYSlider = null!;
|
||||||
|
private ExpandableSlider<float> spacingSlider = null!;
|
||||||
|
private ExpandableSlider<float> gridLinesRotationSlider = null!;
|
||||||
|
|
||||||
|
public OsuGridToolboxGroup()
|
||||||
|
: base("grid")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private const float max_automatic_spacing = 64;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
startPositionXSlider = new ExpandableSlider<float>
|
||||||
|
{
|
||||||
|
Current = StartPositionX,
|
||||||
|
KeyboardStep = 1,
|
||||||
|
},
|
||||||
|
startPositionYSlider = new ExpandableSlider<float>
|
||||||
|
{
|
||||||
|
Current = StartPositionY,
|
||||||
|
KeyboardStep = 1,
|
||||||
|
},
|
||||||
|
spacingSlider = new ExpandableSlider<float>
|
||||||
|
{
|
||||||
|
Current = Spacing,
|
||||||
|
KeyboardStep = 1,
|
||||||
|
},
|
||||||
|
gridLinesRotationSlider = new ExpandableSlider<float>
|
||||||
|
{
|
||||||
|
Current = GridLinesRotation,
|
||||||
|
KeyboardStep = 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Spacing.Value = editorBeatmap.BeatmapInfo.GridSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
StartPositionX.BindValueChanged(x =>
|
||||||
|
{
|
||||||
|
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}";
|
||||||
|
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}";
|
||||||
|
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
StartPositionY.BindValueChanged(y =>
|
||||||
|
{
|
||||||
|
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:N0}";
|
||||||
|
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:N0}";
|
||||||
|
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
Spacing.BindValueChanged(spacing =>
|
||||||
|
{
|
||||||
|
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}";
|
||||||
|
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}";
|
||||||
|
SpacingVector.Value = new Vector2(spacing.NewValue);
|
||||||
|
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
GridLinesRotation.BindValueChanged(rotation =>
|
||||||
|
{
|
||||||
|
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
|
||||||
|
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextGridSize()
|
||||||
|
{
|
||||||
|
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case GlobalAction.EditorCycleGridDisplayMode:
|
||||||
|
nextGridSize();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
@ -65,6 +66,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
[Cached(typeof(IDistanceSnapProvider))]
|
[Cached(typeof(IDistanceSnapProvider))]
|
||||||
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
|
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
|
||||||
|
|
||||||
@ -80,10 +84,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
LayerBelowRuleset.AddRange(new Drawable[]
|
LayerBelowRuleset.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
distanceSnapGridContainer = new Container
|
distanceSnapGridContainer = new Container
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both
|
|
||||||
},
|
|
||||||
rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}
|
}
|
||||||
@ -99,14 +99,38 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
// we may be entering the screen with a selection already active
|
// we may be entering the screen with a selection already active
|
||||||
updateDistanceSnapGrid();
|
updateDistanceSnapGrid();
|
||||||
|
|
||||||
|
updatePositionSnapGrid();
|
||||||
|
|
||||||
RightToolbox.AddRange(new EditorToolboxGroup[]
|
RightToolbox.AddRange(new EditorToolboxGroup[]
|
||||||
{
|
{
|
||||||
new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, },
|
OsuGridToolboxGroup,
|
||||||
|
new TransformToolboxGroup
|
||||||
|
{
|
||||||
|
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||||
|
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||||
|
},
|
||||||
FreehandlSliderToolboxGroup
|
FreehandlSliderToolboxGroup
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updatePositionSnapGrid()
|
||||||
|
{
|
||||||
|
if (positionSnapGrid != null)
|
||||||
|
LayerBelowRuleset.Remove(positionSnapGrid, true);
|
||||||
|
|
||||||
|
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
|
||||||
|
|
||||||
|
rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
|
||||||
|
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
|
||||||
|
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
|
||||||
|
|
||||||
|
positionSnapGrid = rectangularPositionSnapGrid;
|
||||||
|
|
||||||
|
positionSnapGrid.RelativeSizeAxes = Axes.Both;
|
||||||
|
LayerBelowRuleset.Add(positionSnapGrid);
|
||||||
|
}
|
||||||
|
|
||||||
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
||||||
=> new OsuBlueprintContainer(this);
|
=> new OsuBlueprintContainer(this);
|
||||||
|
|
||||||
@ -147,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private readonly Cached distanceSnapGridCache = new Cached();
|
private readonly Cached distanceSnapGridCache = new Cached();
|
||||||
private double? lastDistanceSnapGridTime;
|
private double? lastDistanceSnapGridTime;
|
||||||
|
|
||||||
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
|
private PositionSnapGrid positionSnapGrid;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
@ -205,9 +229,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||||
{
|
{
|
||||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
||||||
|
|
||||||
result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
|
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
|
||||||
|
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
|
||||||
|
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||||
|
|
||||||
|
result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Input.Bindings;
|
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Input.Bindings;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
|
||||||
using osu.Game.Screens.Edit;
|
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit
|
|
||||||
{
|
|
||||||
public partial class OsuRectangularPositionSnapGrid : RectangularPositionSnapGrid, IKeyBindingHandler<GlobalAction>
|
|
||||||
{
|
|
||||||
private static readonly int[] grid_sizes = { 4, 8, 16, 32 };
|
|
||||||
|
|
||||||
private int currentGridSizeIndex = grid_sizes.Length - 1;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
|
||||||
|
|
||||||
public OsuRectangularPositionSnapGrid()
|
|
||||||
: base(OsuPlayfield.BASE_SIZE / 2)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
int gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize);
|
|
||||||
if (gridSizeIndex >= 0)
|
|
||||||
currentGridSizeIndex = gridSizeIndex;
|
|
||||||
updateSpacing();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void nextGridSize()
|
|
||||||
{
|
|
||||||
currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length;
|
|
||||||
updateSpacing();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSpacing()
|
|
||||||
{
|
|
||||||
int gridSize = grid_sizes[currentGridSizeIndex];
|
|
||||||
|
|
||||||
editorBeatmap.BeatmapInfo.GridSize = gridSize;
|
|
||||||
Spacing = new Vector2(gridSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
|
||||||
{
|
|
||||||
switch (e.Action)
|
|
||||||
{
|
|
||||||
case GlobalAction.EditorCycleGridDisplayMode:
|
|
||||||
nextGridSize();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
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,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class OsuSelectionHandler : EditorSelectionHandler
|
public partial class OsuSelectionHandler : EditorSelectionHandler
|
||||||
{
|
{
|
||||||
[Resolved(CanBeNull = true)]
|
|
||||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
|
||||||
/// can be maintained throughout the operation.
|
|
||||||
/// </summary>
|
|
||||||
private List<PathType?>? referencePathTypes;
|
|
||||||
|
|
||||||
protected override void OnSelectionChanged()
|
protected override void OnSelectionChanged()
|
||||||
{
|
{
|
||||||
base.OnSelectionChanged();
|
base.OnSelectionChanged();
|
||||||
|
|
||||||
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||||
|
|
||||||
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
SelectionBox.CanFlipX = quad.Width > 0;
|
||||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
SelectionBox.CanFlipY = quad.Height > 0;
|
||||||
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
|
|
||||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnOperationEnded()
|
|
||||||
{
|
|
||||||
base.OnOperationEnded();
|
|
||||||
referencePathTypes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
||||||
@ -149,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return didFlip;
|
return didFlip;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HandleScale(Vector2 scale, Anchor reference)
|
|
||||||
{
|
|
||||||
adjustScaleFromAnchor(ref scale, reference);
|
|
||||||
|
|
||||||
var hitObjects = selectedMovableObjects;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
|
||||||
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
|
|
||||||
scaleSlider(slider, scale);
|
|
||||||
else
|
|
||||||
scaleHitObjects(hitObjects, reference, scale);
|
|
||||||
|
|
||||||
moveSelectionInBounds();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
|
||||||
{
|
|
||||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
|
||||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
|
||||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
|
||||||
|
|
||||||
// reverse the scale direction if dragging from top or left.
|
|
||||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
|
||||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||||
|
|
||||||
private void scaleSlider(Slider slider, Vector2 scale)
|
public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
|
||||||
{
|
|
||||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
|
||||||
|
|
||||||
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
|
||||||
|
|
||||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
|
||||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
|
||||||
|
|
||||||
Vector2 pathRelativeDeltaScale = new Vector2(
|
|
||||||
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
|
|
||||||
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
|
|
||||||
|
|
||||||
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
|
|
||||||
|
|
||||||
foreach (var point in slider.Path.ControlPoints)
|
|
||||||
{
|
|
||||||
oldControlPoints.Enqueue(point.Position);
|
|
||||||
point.Position *= pathRelativeDeltaScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
|
||||||
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
|
|
||||||
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
|
|
||||||
|
|
||||||
// Snap the slider's length to the current beat divisor
|
|
||||||
// to calculate the final resulting duration / bounding box before the final checks.
|
|
||||||
slider.SnapTo(snapProvider);
|
|
||||||
|
|
||||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
|
||||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
|
||||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
|
||||||
|
|
||||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var point in slider.Path.ControlPoints)
|
|
||||||
point.Position = oldControlPoints.Dequeue();
|
|
||||||
|
|
||||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
|
||||||
slider.SnapTo(snapProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
|
||||||
{
|
|
||||||
scale = getClampedScale(hitObjects, reference, scale);
|
|
||||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
|
||||||
|
|
||||||
foreach (var h in hitObjects)
|
|
||||||
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
|
||||||
{
|
|
||||||
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
|
|
||||||
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
|
|
||||||
|
|
||||||
return (xInBounds, yInBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void moveSelectionInBounds()
|
private void moveSelectionInBounds()
|
||||||
{
|
{
|
||||||
@ -262,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
h.Position += delta;
|
h.Position += delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObjects">The hitobjects to be scaled</param>
|
|
||||||
/// <param name="reference">The anchor from which the scale operation is performed</param>
|
|
||||||
/// <param name="scale">The scale to be clamped</param>
|
|
||||||
/// <returns>The clamped scale vector</returns>
|
|
||||||
private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
|
||||||
{
|
|
||||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
|
||||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
|
||||||
|
|
||||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
|
||||||
|
|
||||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
|
||||||
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
|
||||||
|
|
||||||
//max Size -> playfield bounds
|
|
||||||
if (scaledQuad.TopLeft.X < 0)
|
|
||||||
scale.X += scaledQuad.TopLeft.X;
|
|
||||||
if (scaledQuad.TopLeft.Y < 0)
|
|
||||||
scale.Y += scaledQuad.TopLeft.Y;
|
|
||||||
|
|
||||||
if (scaledQuad.BottomRight.X > DrawWidth)
|
|
||||||
scale.X -= scaledQuad.BottomRight.X - DrawWidth;
|
|
||||||
if (scaledQuad.BottomRight.Y > DrawHeight)
|
|
||||||
scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
|
|
||||||
|
|
||||||
//min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
|
|
||||||
Vector2 scaledSize = selectionQuad.Size + scale;
|
|
||||||
Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
|
|
||||||
|
|
||||||
scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
|
|
||||||
|
|
||||||
return scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||||
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
CanRotateAroundSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
||||||
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any();
|
CanRotateAroundPlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private OsuHitObject[]? objectsInRotation;
|
private OsuHitObject[]? objectsInRotation;
|
||||||
@ -53,9 +53,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public override void Begin()
|
public override void Begin()
|
||||||
{
|
{
|
||||||
if (objectsInRotation != null)
|
if (OperationInProgress.Value)
|
||||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
||||||
|
|
||||||
|
base.Begin();
|
||||||
|
|
||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
objectsInRotation = selectedMovableObjects.ToArray();
|
objectsInRotation = selectedMovableObjects.ToArray();
|
||||||
@ -68,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public override void Update(float rotation, Vector2? origin = null)
|
public override void Update(float rotation, Vector2? origin = null)
|
||||||
{
|
{
|
||||||
if (objectsInRotation == null)
|
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(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;
|
||||||
|
|
||||||
@ -91,11 +93,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
public override void Commit()
|
public override void Commit()
|
||||||
{
|
{
|
||||||
if (objectsInRotation == null)
|
if (!OperationInProgress.Value)
|
||||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
|
|
||||||
|
base.Commit();
|
||||||
|
|
||||||
objectsInRotation = null;
|
objectsInRotation = null;
|
||||||
originalPositions = null;
|
originalPositions = null;
|
||||||
originalPathControlPointPositions = null;
|
originalPathControlPointPositions = null;
|
||||||
|
245
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
245
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether scaling anchored by the center of the playfield can currently be performed.
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<bool> CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a single slider is currently selected, which results in a different scaling behaviour.
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||||
|
|
||||||
|
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(EditorBeatmap editorBeatmap)
|
||||||
|
{
|
||||||
|
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||||
|
|
||||||
|
CanScaleX.Value = quad.Width > 0;
|
||||||
|
CanScaleY.Value = quad.Height > 0;
|
||||||
|
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
|
||||||
|
CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||||
|
IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||||
|
private Vector2? defaultOrigin;
|
||||||
|
|
||||||
|
public override void Begin()
|
||||||
|
{
|
||||||
|
if (OperationInProgress.Value)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||||
|
|
||||||
|
base.Begin();
|
||||||
|
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
|
||||||
|
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
||||||
|
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||||
|
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||||
|
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||||
|
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||||
|
{
|
||||||
|
if (!OperationInProgress.Value)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
|
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||||
|
|
||||||
|
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||||
|
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||||
|
{
|
||||||
|
var originalInfo = objectsInScale[slider];
|
||||||
|
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||||
|
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
|
||||||
|
|
||||||
|
foreach (var (ho, originalState) in objectsInScale)
|
||||||
|
{
|
||||||
|
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveSelectionInBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Commit()
|
||||||
|
{
|
||||||
|
if (!OperationInProgress.Value)
|
||||||
|
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||||
|
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
|
||||||
|
base.Commit();
|
||||||
|
|
||||||
|
objectsInScale = null;
|
||||||
|
OriginalSurroundingQuad = null;
|
||||||
|
defaultOrigin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||||
|
.Where(h => h is not Spinner);
|
||||||
|
|
||||||
|
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
|
||||||
|
{
|
||||||
|
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||||
|
|
||||||
|
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||||
|
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||||
|
{
|
||||||
|
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
|
||||||
|
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the slider's length to the current beat divisor
|
||||||
|
// to calculate the final resulting duration / bounding box before the final checks.
|
||||||
|
slider.SnapTo(snapProvider);
|
||||||
|
|
||||||
|
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||||
|
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||||
|
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||||
|
|
||||||
|
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||||
|
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
|
||||||
|
|
||||||
|
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||||
|
slider.SnapTo(snapProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||||
|
{
|
||||||
|
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X);
|
||||||
|
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y);
|
||||||
|
|
||||||
|
return (xInBounds, yInBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||||
|
/// <param name="scale">The scale to be clamped</param>
|
||||||
|
/// <returns>The clamped scale vector</returns>
|
||||||
|
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
|
||||||
|
{
|
||||||
|
//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)
|
||||||
|
return scale;
|
||||||
|
|
||||||
|
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||||
|
|
||||||
|
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||||
|
origin = slider.Position;
|
||||||
|
|
||||||
|
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||||
|
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||||
|
|
||||||
|
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||||
|
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||||
|
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||||
|
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||||
|
|
||||||
|
if (!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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveSelectionInBounds()
|
||||||
|
{
|
||||||
|
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
|
||||||
|
|
||||||
|
Vector2 delta = Vector2.Zero;
|
||||||
|
|
||||||
|
if (quad.TopLeft.X < 0)
|
||||||
|
delta.X -= quad.TopLeft.X;
|
||||||
|
if (quad.TopLeft.Y < 0)
|
||||||
|
delta.Y -= quad.TopLeft.Y;
|
||||||
|
|
||||||
|
if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X)
|
||||||
|
delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X;
|
||||||
|
if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||||
|
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
|
||||||
|
|
||||||
|
foreach (var (h, _) in objectsInScale!)
|
||||||
|
h.Position += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct OriginalHitObjectState
|
||||||
|
{
|
||||||
|
public Vector2 Position { get; }
|
||||||
|
public Vector2[]? PathControlPointPositions { get; }
|
||||||
|
public PathType?[]? PathControlPointTypes { get; }
|
||||||
|
|
||||||
|
public OriginalHitObjectState(OsuHitObject hitObject)
|
||||||
|
{
|
||||||
|
Position = hitObject.Position;
|
||||||
|
PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray();
|
||||||
|
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,11 +78,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ScheduleAfterChildren(() => angleInput.TakeFocus());
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
angleInput.TakeFocus();
|
||||||
|
angleInput.SelectAll();
|
||||||
|
});
|
||||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||||
rotationOrigin.Items.First().Select();
|
rotationOrigin.Items.First().Select();
|
||||||
|
|
||||||
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e =>
|
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||||
{
|
{
|
||||||
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
212
osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
Normal file
212
osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class PreciseScalePopover : OsuPopover
|
||||||
|
{
|
||||||
|
private readonly OsuSelectionScaleHandler scaleHandler;
|
||||||
|
|
||||||
|
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
|
||||||
|
|
||||||
|
private SliderWithTextBoxInput<float> scaleInput = null!;
|
||||||
|
private BindableNumber<float> scaleInputBindable = null!;
|
||||||
|
private EditorRadioButtonCollection scaleOrigin = null!;
|
||||||
|
|
||||||
|
private RadioButton playfieldCentreButton = null!;
|
||||||
|
private RadioButton selectionCentreButton = null!;
|
||||||
|
|
||||||
|
private OsuCheckbox xCheckBox = null!;
|
||||||
|
private OsuCheckbox yCheckBox = null!;
|
||||||
|
|
||||||
|
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
|
||||||
|
{
|
||||||
|
this.scaleHandler = scaleHandler;
|
||||||
|
|
||||||
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 220,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
scaleInput = new SliderWithTextBoxInput<float>("Scale:")
|
||||||
|
{
|
||||||
|
Current = scaleInputBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
MinValue = 0.5f,
|
||||||
|
MaxValue = 2,
|
||||||
|
Precision = 0.001f,
|
||||||
|
Value = 1,
|
||||||
|
Default = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true
|
||||||
|
},
|
||||||
|
scaleOrigin = new EditorRadioButtonCollection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Items = new[]
|
||||||
|
{
|
||||||
|
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||||
|
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||||
|
selectionCentreButton = new RadioButton("Selection centre",
|
||||||
|
() => setOrigin(ScaleOrigin.SelectionCentre),
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(4),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
xCheckBox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "X-axis",
|
||||||
|
Current = { Value = true },
|
||||||
|
},
|
||||||
|
yCheckBox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "Y-axis",
|
||||||
|
Current = { Value = true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
|
{
|
||||||
|
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
|
||||||
|
};
|
||||||
|
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
|
{
|
||||||
|
selectionCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to its centre." : string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
scaleInput.TakeFocus();
|
||||||
|
scaleInput.SelectAll();
|
||||||
|
});
|
||||||
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
|
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
|
||||||
|
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
|
||||||
|
|
||||||
|
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||||
|
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||||
|
|
||||||
|
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||||
|
|
||||||
|
scaleInfo.BindValueChanged(scale =>
|
||||||
|
{
|
||||||
|
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
|
||||||
|
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAxisCheckBoxesEnabled()
|
||||||
|
{
|
||||||
|
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
|
||||||
|
{
|
||||||
|
toggleAxisAvailable(xCheckBox.Current, true);
|
||||||
|
toggleAxisAvailable(yCheckBox.Current, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toggleAxisAvailable(xCheckBox.Current, scaleHandler.CanScaleX.Value);
|
||||||
|
toggleAxisAvailable(yCheckBox.Current, scaleHandler.CanScaleY.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleAxisAvailable(Bindable<bool> axisBindable, bool available)
|
||||||
|
{
|
||||||
|
// enable the bindable to allow setting the value
|
||||||
|
axisBindable.Disabled = false;
|
||||||
|
// restore the presumed default value given the axis's new availability state
|
||||||
|
axisBindable.Value = available;
|
||||||
|
axisBindable.Disabled = !available;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMaxScale()
|
||||||
|
{
|
||||||
|
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float max_scale = 10;
|
||||||
|
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
|
||||||
|
|
||||||
|
if (!scaleInfo.Value.XAxis)
|
||||||
|
scale.X = max_scale;
|
||||||
|
if (!scaleInfo.Value.YAxis)
|
||||||
|
scale.Y = max_scale;
|
||||||
|
|
||||||
|
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setOrigin(ScaleOrigin origin)
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||||
|
updateMaxScale();
|
||||||
|
updateAxisCheckBoxesEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
|
||||||
|
|
||||||
|
private void setAxis(bool x, bool y)
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
||||||
|
updateMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
scaleHandler.Begin();
|
||||||
|
updateMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
|
if (IsLoaded) scaleHandler.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ScaleOrigin
|
||||||
|
{
|
||||||
|
PlayfieldCentre,
|
||||||
|
SelectionCentre
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis);
|
||||||
|
}
|
@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
|||||||
private void updateBeatmap()
|
private void updateBeatmap()
|
||||||
{
|
{
|
||||||
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
|
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
|
||||||
|
Beatmap.UpdateAllHitObjects();
|
||||||
Beatmap.SaveState();
|
Beatmap.SaveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,14 +18,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
private readonly Bindable<bool> canRotate = new BindableBool();
|
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
|
||||||
private EditorToolButton rotateButton = null!;
|
private EditorToolButton rotateButton = null!;
|
||||||
|
private EditorToolButton scaleButton = null!;
|
||||||
private Bindable<bool> canRotatePlayfieldOrigin = null!;
|
|
||||||
private Bindable<bool> canRotateSelectionOrigin = null!;
|
|
||||||
|
|
||||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||||
|
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
|
||||||
|
|
||||||
public TransformToolboxGroup()
|
public TransformToolboxGroup()
|
||||||
: base("transform")
|
: base("transform")
|
||||||
@ -45,7 +45,9 @@ 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)),
|
||||||
// TODO: scale
|
scaleButton = new EditorToolButton("Scale",
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||||
|
() => new PreciseScalePopover(ScaleHandler))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -54,21 +56,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
// aggregate two values into canRotate
|
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
||||||
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy();
|
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
||||||
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
|
||||||
|
|
||||||
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy();
|
canScale.AddSource(ScaleHandler.CanScaleX);
|
||||||
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
canScale.AddSource(ScaleHandler.CanScaleY);
|
||||||
|
canScale.AddSource(ScaleHandler.CanScaleFromPlayfieldOrigin);
|
||||||
void updateCanRotateAggregate()
|
|
||||||
{
|
|
||||||
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||||
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
|
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
||||||
|
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
@ -79,7 +77,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
case GlobalAction.EditorToggleRotateControl:
|
case GlobalAction.EditorToggleRotateControl:
|
||||||
{
|
{
|
||||||
rotateButton.TriggerClick();
|
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
||||||
|
rotateButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case GlobalAction.EditorToggleScaleControl:
|
||||||
|
{
|
||||||
|
if (!ScaleHandler.OperationInProgress.Value || scaleButton.Selected.Value)
|
||||||
|
scaleButton.TriggerClick();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,8 +67,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
// Generate the replay frames the cursor should follow
|
// Generate the replay frames the cursor should follow
|
||||||
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
|
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
|
||||||
|
|
||||||
drawableRuleset.UseResumeOverlay = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
// Hide judgment displays and follow points as they won't make any sense.
|
// Hide follow points as they won't make any sense.
|
||||||
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
||||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
|
||||||
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,9 +39,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
// Hide judgment displays and follow points as they won't make any sense.
|
// Hide follow points as they won't make any sense.
|
||||||
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
||||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
|
||||||
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
// Hide judgment displays and follow points as they won't make any sense.
|
// Hide follow points as they won't make any sense.
|
||||||
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
||||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
|
||||||
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override LocalisableString Description => "Put your faith in the approach circles...";
|
public override LocalisableString Description => "Put your faith in the approach circles...";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
|
public override bool Ranked => true;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
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.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
@ -19,6 +20,7 @@ using osu.Game.Rulesets.Osu.UI;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -319,5 +321,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
|
||||||
|
|
||||||
|
internal void SuppressHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Idle);
|
||||||
|
UpdateComboColour();
|
||||||
|
|
||||||
|
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||||
|
|
||||||
|
if (Time.Current >= HitStateUpdateTime)
|
||||||
|
{
|
||||||
|
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
|
||||||
|
AccentColour.Value = Color4.White;
|
||||||
|
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
LifetimeEnd = HitStateUpdateTime + 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RestoreHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Hit, force: true);
|
||||||
|
UpdateComboColour();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -14,10 +12,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
public partial class DrawableOsuJudgement : DrawableJudgement
|
public partial class DrawableOsuJudgement : DrawableJudgement
|
||||||
{
|
{
|
||||||
internal SkinnableLighting Lighting { get; private set; }
|
internal SkinnableLighting Lighting { get; private set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; }
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
|
private bool positionTransferred;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -39,10 +39,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Lighting.ResetAnimation();
|
Lighting.ResetAnimation();
|
||||||
Lighting.SetColourFrom(JudgedObject, Result);
|
Lighting.SetColourFrom(JudgedObject, Result);
|
||||||
|
|
||||||
if (JudgedObject?.HitObject is OsuHitObject osuObject)
|
positionTransferred = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
|
||||||
{
|
{
|
||||||
Position = osuObject.StackedEndPosition;
|
switch (osuObject)
|
||||||
Scale = new Vector2(osuObject.Scale);
|
{
|
||||||
|
case DrawableSlider slider:
|
||||||
|
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
positionTransferred = true;
|
||||||
|
|
||||||
|
Scale = new Vector2(osuObject.HitObject.Scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,5 +370,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
private partial class DefaultSliderBody : PlaySliderBody
|
private partial class DefaultSliderBody : PlaySliderBody
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
|
||||||
|
|
||||||
|
internal void SuppressHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Idle);
|
||||||
|
HeadCircle.SuppressHitAnimations();
|
||||||
|
TailCircle.SuppressHitAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RestoreHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Hit);
|
||||||
|
HeadCircle.RestoreHitAnimations();
|
||||||
|
TailCircle.RestoreHitAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -63,22 +61,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
base.ApplyTransformsAt(time, false);
|
base.ApplyTransformsAt(time, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2? lastPosition;
|
|
||||||
|
|
||||||
public void UpdateProgress(double completionProgress)
|
public void UpdateProgress(double completionProgress)
|
||||||
{
|
{
|
||||||
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
|
Slider slider = drawableSlider.HitObject;
|
||||||
|
Position = slider.CurvePositionAt(completionProgress);
|
||||||
|
|
||||||
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
|
//0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
|
||||||
|
var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance));
|
||||||
bool rewinding = (Clock as IGameplayClock)?.IsRewinding == true;
|
|
||||||
|
|
||||||
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
||||||
|
// Needed for when near completion, or in case of a very short slider.
|
||||||
if (diff.LengthFast < 0.01f)
|
if (diff.LengthFast < 0.01f)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0);
|
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
|
||||||
lastPosition = Position;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,12 @@ using JetBrains.Annotations;
|
|||||||
using osu.Framework.Allocation;
|
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.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -125,5 +127,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
if (Slider != null)
|
if (Slider != null)
|
||||||
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
|
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
|
||||||
|
|
||||||
|
internal void SuppressHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Idle);
|
||||||
|
UpdateComboColour();
|
||||||
|
|
||||||
|
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||||
|
|
||||||
|
if (Time.Current >= HitStateUpdateTime)
|
||||||
|
{
|
||||||
|
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
|
||||||
|
AccentColour.Value = Color4.White;
|
||||||
|
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
LifetimeEnd = HitStateUpdateTime + 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RestoreHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Hit);
|
||||||
|
UpdateComboColour();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,6 +252,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
protected void UpdateNestedSamples()
|
protected void UpdateNestedSamples()
|
||||||
{
|
{
|
||||||
|
this.PopulateNodeSamples();
|
||||||
|
|
||||||
// TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
|
// TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
|
||||||
HitSampleInfo tickSample = (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault())?.With("slidertick");
|
HitSampleInfo tickSample = (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault())?.With("slidertick");
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
//
|
//
|
||||||
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
|
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
|
||||||
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
|
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
|
||||||
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition));
|
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.CanBeHit() && c.ReceivePositionalInputAt(screenSpacePosition));
|
||||||
|
|
||||||
public OsuInputManager(RulesetInfo ruleset)
|
public OsuInputManager(RulesetInfo ruleset)
|
||||||
: base(ruleset, 0, SimultaneousBindingMode.Unique)
|
: base(ruleset, 0, SimultaneousBindingMode.Unique)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
@ -11,10 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ManualSliderBody : SliderBody
|
public partial class ManualSliderBody : SliderBody
|
||||||
{
|
{
|
||||||
public new void SetVertices(IReadOnlyList<Vector2> vertices)
|
public ManualSliderBody()
|
||||||
{
|
{
|
||||||
base.SetVertices(vertices);
|
AutoSizeAxes = Axes.Both;
|
||||||
Size = Path.Size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public new void SetVertices(IReadOnlyList<Vector2> vertices) => base.SetVertices(vertices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Replays;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Configuration;
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -45,7 +46,13 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true };
|
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true };
|
||||||
|
|
||||||
protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay();
|
protected override ResumeOverlay CreateResumeOverlay()
|
||||||
|
{
|
||||||
|
if (Mods.Any(m => m is OsuModAutopilot))
|
||||||
|
return new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
|
||||||
|
|
||||||
|
return new OsuResumeOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay);
|
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay);
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
using osu.Game.Rulesets.Osu.Configuration;
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -27,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
[Cached]
|
[Cached]
|
||||||
public partial class OsuPlayfield : Playfield
|
public partial class OsuPlayfield : Playfield
|
||||||
{
|
{
|
||||||
|
private readonly Container borderContainer;
|
||||||
private readonly PlayfieldBorder playfieldBorder;
|
private readonly PlayfieldBorder playfieldBorder;
|
||||||
private readonly ProxyContainer approachCircles;
|
private readonly ProxyContainer approachCircles;
|
||||||
private readonly ProxyContainer spinnerProxies;
|
private readonly ProxyContainer spinnerProxies;
|
||||||
@ -54,7 +56,11 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
borderContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||||
|
},
|
||||||
Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
|
Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
|
||||||
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
||||||
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
|
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
|
||||||
@ -151,6 +157,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
RegisterPool<Spinner, DrawableSpinner>(2, 20);
|
RegisterPool<Spinner, DrawableSpinner>(2, 20);
|
||||||
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
|
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
|
||||||
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 200);
|
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 200);
|
||||||
|
|
||||||
|
if (beatmap != null)
|
||||||
|
ApplyCircleSizeToPlayfieldBorder(beatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ApplyCircleSizeToPlayfieldBorder(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
borderContainer.Padding = new MarginPadding(OsuHitObject.OBJECT_RADIUS * -LegacyRulesetExtensions.CalculateScaleFromCircleSize(beatmap.Difficulty.CircleSize, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
|
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
|
||||||
|
@ -85,6 +85,42 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
AssertResult<Swell>(0, HitResult.IgnoreMiss);
|
AssertResult<Swell>(0, HitResult.IgnoreMiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAlternatingIsRequired()
|
||||||
|
{
|
||||||
|
const double hit_time = 1000;
|
||||||
|
|
||||||
|
Swell swell = new Swell
|
||||||
|
{
|
||||||
|
StartTime = hit_time,
|
||||||
|
Duration = 1000,
|
||||||
|
RequiredHits = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(0),
|
||||||
|
new TaikoReplayFrame(2001),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < swell.RequiredHits; i++)
|
||||||
|
{
|
||||||
|
double frameTime = 1000 + i * 50;
|
||||||
|
frames.Add(new TaikoReplayFrame(frameTime, TaikoAction.LeftCentre));
|
||||||
|
frames.Add(new TaikoReplayFrame(frameTime + 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
PerformTest(frames, CreateBeatmap(swell));
|
||||||
|
|
||||||
|
AssertJudgementCount(11);
|
||||||
|
|
||||||
|
AssertResult<SwellTick>(0, HitResult.IgnoreHit);
|
||||||
|
for (int i = 1; i < swell.RequiredHits; i++)
|
||||||
|
AssertResult<SwellTick>(i, HitResult.IgnoreMiss);
|
||||||
|
|
||||||
|
AssertResult<Swell>(0, HitResult.IgnoreMiss);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitNoneSwell()
|
public void TestHitNoneSwell()
|
||||||
{
|
{
|
||||||
|
46
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModRelax.cs
Normal file
46
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModRelax.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Replays;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||||
|
{
|
||||||
|
public partial class TestSceneTaikoModRelax : TaikoModTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestRelax()
|
||||||
|
{
|
||||||
|
var beatmap = new TaikoBeatmap
|
||||||
|
{
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Hit { StartTime = 0, Type = HitType.Centre, },
|
||||||
|
new Hit { StartTime = 250, Type = HitType.Rim, },
|
||||||
|
new DrumRoll { StartTime = 500, Duration = 500, },
|
||||||
|
new Swell { StartTime = 1250, Duration = 500 },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
foreach (var ho in beatmap.HitObjects)
|
||||||
|
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||||
|
|
||||||
|
var replay = new TaikoAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
foreach (var frame in replay.Frames.OfType<TaikoReplayFrame>().Where(r => r.Actions.Any()))
|
||||||
|
frame.Actions = [TaikoAction.LeftCentre];
|
||||||
|
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new TaikoModRelax(),
|
||||||
|
Beatmap = beatmap,
|
||||||
|
ReplayFrames = replay.Frames,
|
||||||
|
Autoplay = false,
|
||||||
|
PassCondition = () => Player.ScoreProcessor.HasCompleted.Value && Player.ScoreProcessor.Accuracy.Value == 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
|||||||
Autoplay = false,
|
Autoplay = false,
|
||||||
Beatmap = new Beatmap
|
Beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
Breaks = new List<BreakPeriod>
|
Breaks =
|
||||||
{
|
{
|
||||||
new BreakPeriod(100, 1600),
|
new BreakPeriod(100, 1600),
|
||||||
},
|
},
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|
||||||
{
|
|
||||||
public class Peaks : Skill
|
|
||||||
{
|
|
||||||
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
|
|
||||||
private const double colour_skill_multiplier = 0.375 * final_multiplier;
|
|
||||||
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
|
|
||||||
|
|
||||||
private const double final_multiplier = 0.0625;
|
|
||||||
|
|
||||||
private readonly Rhythm rhythm;
|
|
||||||
private readonly Colour colour;
|
|
||||||
private readonly Stamina stamina;
|
|
||||||
|
|
||||||
public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier;
|
|
||||||
public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
|
||||||
public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier;
|
|
||||||
|
|
||||||
public Peaks(Mod[] mods)
|
|
||||||
: base(mods)
|
|
||||||
{
|
|
||||||
rhythm = new Rhythm(mods);
|
|
||||||
colour = new Colour(mods);
|
|
||||||
stamina = new Stamina(mods);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
|
||||||
/// <param name="values">The coefficients of the vector.</param>
|
|
||||||
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
|
||||||
|
|
||||||
public override void Process(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
rhythm.Process(current);
|
|
||||||
colour.Process(current);
|
|
||||||
stamina.Process(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
|
||||||
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
|
||||||
/// </remarks>
|
|
||||||
public override double DifficultyValue()
|
|
||||||
{
|
|
||||||
List<double> peaks = new List<double>();
|
|
||||||
|
|
||||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
|
||||||
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
|
||||||
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
|
||||||
|
|
||||||
for (int i = 0; i < colourPeaks.Count; i++)
|
|
||||||
{
|
|
||||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
|
||||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
|
||||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
|
|
||||||
|
|
||||||
double peak = norm(1.5, colourPeak, staminaPeak);
|
|
||||||
peak = norm(2, peak, rhythmPeak);
|
|
||||||
|
|
||||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
|
||||||
// These sections will not contribute to the difficulty.
|
|
||||||
if (peak > 0)
|
|
||||||
peaks.Add(peak);
|
|
||||||
}
|
|
||||||
|
|
||||||
double difficulty = 0;
|
|
||||||
double weight = 1;
|
|
||||||
|
|
||||||
foreach (double strain in peaks.OrderDescending())
|
|
||||||
{
|
|
||||||
difficulty += strain * weight;
|
|
||||||
weight *= 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
return difficulty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -23,6 +23,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
private const double difficulty_multiplier = 1.35;
|
private const double difficulty_multiplier = 1.35;
|
||||||
|
|
||||||
|
private const double final_multiplier = 0.0625;
|
||||||
|
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
|
||||||
|
private const double colour_skill_multiplier = 0.375 * final_multiplier;
|
||||||
|
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
|
||||||
|
|
||||||
public override int Version => 20221107;
|
public override int Version => 20221107;
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
@ -34,7 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
return new Skill[]
|
return new Skill[]
|
||||||
{
|
{
|
||||||
new Peaks(mods)
|
new Rhythm(mods),
|
||||||
|
new Colour(mods),
|
||||||
|
new Stamina(mods)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,13 +79,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
if (beatmap.HitObjects.Count == 0)
|
if (beatmap.HitObjects.Count == 0)
|
||||||
return new TaikoDifficultyAttributes { Mods = mods };
|
return new TaikoDifficultyAttributes { Mods = mods };
|
||||||
|
|
||||||
var combined = (Peaks)skills[0];
|
Colour colour = (Colour)skills.First(x => x is Colour);
|
||||||
|
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||||
|
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||||
|
|
||||||
double colourRating = combined.ColourDifficultyValue * difficulty_multiplier;
|
double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
|
||||||
double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier;
|
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
|
||||||
double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier;
|
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier;
|
||||||
|
|
||||||
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
|
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier;
|
||||||
double starRating = rescale(combinedRating * 1.4);
|
double starRating = rescale(combinedRating * 1.4);
|
||||||
|
|
||||||
HitWindows hitWindows = new TaikoHitWindows();
|
HitWindows hitWindows = new TaikoHitWindows();
|
||||||
@ -109,5 +118,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
return 10.43 * Math.Log(sr / 8 + 1);
|
return 10.43 * Math.Log(sr / 8 + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
||||||
|
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||||
|
/// </remarks>
|
||||||
|
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
|
||||||
|
{
|
||||||
|
List<double> peaks = new List<double>();
|
||||||
|
|
||||||
|
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||||
|
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
||||||
|
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < colourPeaks.Count; i++)
|
||||||
|
{
|
||||||
|
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||||
|
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
||||||
|
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
|
||||||
|
|
||||||
|
double peak = norm(1.5, colourPeak, staminaPeak);
|
||||||
|
peak = norm(2, peak, rhythmPeak);
|
||||||
|
|
||||||
|
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
|
// These sections will not contribute to the difficulty.
|
||||||
|
if (peak > 0)
|
||||||
|
peaks.Add(peak);
|
||||||
|
}
|
||||||
|
|
||||||
|
double difficulty = 0;
|
||||||
|
double weight = 1;
|
||||||
|
|
||||||
|
foreach (double strain in peaks.OrderDescending())
|
||||||
|
{
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
||||||
|
/// <param name="values">The coefficients of the vector.</param>
|
||||||
|
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
|
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double ComputeTimeRange()
|
|
||||||
{
|
|
||||||
// Adjust when we're using constant algorithm to not be sluggish.
|
|
||||||
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
|
|
||||||
return base.ComputeTimeRange() / multiplier;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,9 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
public void SetStrongState(bool state)
|
public void SetStrongState(bool state)
|
||||||
{
|
{
|
||||||
|
if (SelectedItems.OfType<Hit>().All(h => h.IsStrong == state))
|
||||||
|
return;
|
||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(h =>
|
EditorBeatmap.PerformOnSelection(h =>
|
||||||
{
|
{
|
||||||
if (!(h is Hit taikoHit)) return;
|
if (!(h is Hit taikoHit)) return;
|
||||||
@ -67,6 +70,9 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
|
|
||||||
public void SetRimState(bool state)
|
public void SetRimState(bool state)
|
||||||
{
|
{
|
||||||
|
if (SelectedItems.OfType<Hit>().All(h => h.Type == (state ? HitType.Rim : HitType.Centre)))
|
||||||
|
return;
|
||||||
|
|
||||||
EditorBeatmap.PerformOnSelection(h =>
|
EditorBeatmap.PerformOnSelection(h =>
|
||||||
{
|
{
|
||||||
if (h is Hit taikoHit)
|
if (h is Hit taikoHit)
|
||||||
|
29
osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs
Normal file
29
osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.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 osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
|
{
|
||||||
|
public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset<TaikoHitObject>
|
||||||
|
{
|
||||||
|
public override string Name => "Constant Speed";
|
||||||
|
public override string Acronym => "CS";
|
||||||
|
public override double ScoreMultiplier => 0.9;
|
||||||
|
public override LocalisableString Description => "No more tricky speed changes!";
|
||||||
|
public override IconUsage? Icon => FontAwesome.Solid.Equals;
|
||||||
|
public override ModType Type => ModType.Conversion;
|
||||||
|
|
||||||
|
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||||
|
{
|
||||||
|
var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||||
|
taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,13 +5,34 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Mods
|
namespace osu.Game.Rulesets.Taiko.Mods
|
||||||
{
|
{
|
||||||
public class TaikoModRelax : ModRelax
|
public class TaikoModRelax : ModRelax, IApplicableToDrawableHitObject
|
||||||
{
|
{
|
||||||
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
|
public override LocalisableString Description => @"No need to remember which key is correct anymore!";
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||||
|
|
||||||
|
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||||
|
{
|
||||||
|
var allActions = Enum.GetValues<TaikoAction>();
|
||||||
|
|
||||||
|
drawable.HitObjectApplied += dho =>
|
||||||
|
{
|
||||||
|
switch (dho)
|
||||||
|
{
|
||||||
|
case DrawableHit hit:
|
||||||
|
hit.HitActions = allActions;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DrawableSwell swell:
|
||||||
|
swell.MustAlternate = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A list of keys which can result in hits for this HitObject.
|
/// A list of keys which can result in hits for this HitObject.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TaikoAction[] HitActions { get; private set; }
|
public TaikoAction[] HitActions { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The action that caused this <see cref="DrawableHit"/> to be hit.
|
/// The action that caused this <see cref="DrawableHit"/> to be hit.
|
||||||
|
@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
|
|
||||||
public override bool DisplayResult => false;
|
public override bool DisplayResult => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the player must alternate centre and rim hits.
|
||||||
|
/// </summary>
|
||||||
|
public bool MustAlternate { get; internal set; } = true;
|
||||||
|
|
||||||
public DrawableSwell()
|
public DrawableSwell()
|
||||||
: this(null)
|
: this(null)
|
||||||
{
|
{
|
||||||
@ -292,7 +297,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
|
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
|
||||||
|
|
||||||
// Ensure alternating centre and rim hits
|
// Ensure alternating centre and rim hits
|
||||||
if (lastWasCentre == isCentre)
|
if (lastWasCentre == isCentre && MustAlternate)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// If we've already successfully judged a tick this frame, do not judge more.
|
// If we've already successfully judged a tick this frame, do not judge more.
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
Scale = new Vector2(0.7f),
|
Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
|
||||||
Colour = new Colour4(255, 228, 0, 255),
|
Colour = new Colour4(255, 228, 0, 255),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
if (!result.IsHit || !isKiaiActive)
|
if (!result.IsHit || !isKiaiActive)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
sprite.ScaleTo(0.85f).Then()
|
sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
|
||||||
.ScaleTo(0.7f, 80, Easing.OutQuad);
|
.ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class TaikoLegacyHitTarget : CompositeDrawable
|
public partial class TaikoLegacyHitTarget : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// In stable this is 0.7f (see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L592)
|
||||||
|
/// but for whatever reason this doesn't match visually.
|
||||||
|
/// </summary>
|
||||||
|
public const float SCALE = 0.8f;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin)
|
private void load(ISkinSource skin)
|
||||||
{
|
{
|
||||||
@ -22,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
Texture = skin.GetTexture("approachcircle"),
|
Texture = skin.GetTexture("approachcircle"),
|
||||||
Scale = new Vector2(0.83f),
|
Scale = new Vector2(SCALE + 0.03f),
|
||||||
Alpha = 0.47f, // eyeballed to match stable
|
Alpha = 0.47f, // eyeballed to match stable
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -30,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
Texture = skin.GetTexture("taikobigcircle"),
|
Texture = skin.GetTexture("taikobigcircle"),
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(SCALE),
|
||||||
Alpha = 0.22f, // eyeballed to match stable
|
Alpha = 0.22f, // eyeballed to match stable
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
new TaikoModClassic(),
|
new TaikoModClassic(),
|
||||||
new TaikoModSwap(),
|
new TaikoModSwap(),
|
||||||
new TaikoModSingleTap(),
|
new TaikoModSingleTap(),
|
||||||
|
new TaikoModConstantSpeed(),
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.Automation:
|
case ModType.Automation:
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user