1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 08:07:24 +08:00

Merge branch 'master' into carousel-maintain-selection-over-update

This commit is contained in:
Salman Ahmed 2022-08-26 14:35:19 +03:00
commit f38194fb40
228 changed files with 3734 additions and 1745 deletions

View File

@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2022.607.0",
"version": "2022.809.0",
"commands": [
"localisation"
]

View File

@ -53,3 +53,7 @@ dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
# See: https://github.com/ppy/osu/pull/19677
dotnet_diagnostic.OSUF001.severity = none

View File

@ -21,8 +21,11 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
: base(beatmap, ruleset)
{
minPosition = beatmap.HitObjects.Min(getUsablePosition);
maxPosition = beatmap.HitObjects.Max(getUsablePosition);
if (beatmap.HitObjects.Any())
{
minPosition = beatmap.HitObjects.Min(getUsablePosition);
maxPosition = beatmap.HitObjects.Max(getUsablePosition);
}
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition);

View File

@ -51,11 +51,11 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.810.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.810.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.819.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.825.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.14.0" />
<PackageReference Include="Realm" Version="10.15.1" />
</ItemGroup>
</Project>

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Rulesets.Edit;
@ -162,7 +163,7 @@ namespace osu.Game.Rulesets.Catch
};
}
public override string GetDisplayNameForHitResult(HitResult result)
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModEasy : ModEasyWithExtraLives
{
public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override string Name => "Floating Fruits";
public override string Acronym => "FF";
public override string Description => "The fruits are... floating?";
public override LocalisableString Description => "The fruits are... floating?";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Cloud;

View File

@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
{
public override string Description => @"Play with fading fruits.";
public override LocalisableString Description => @"Play with fading fruits.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
private const double fade_out_offset_multiplier = 0.6;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModMirror : ModMirror, IApplicableToBeatmap
{
public override string Description => "Fruits are flipped horizontally.";
public override LocalisableString Description => "Fruits are flipped horizontally.";
/// <remarks>
/// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>,

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Framework.Utils;
using osu.Game.Configuration;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
{
public override string Description => "Where's the catcher?";
public override LocalisableString Description => "Where's the catcher?";
[SettingSource(
"Hidden at combo",

View File

@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
{
public override string Description => @"Use the mouse to control the catcher.";
public override LocalisableString Description => @"Use the mouse to control the catcher.";
private DrawableRuleset<CatchHitObject> drawableRuleset = null!;

View File

@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, 242, "diffcalc-test")]
[TestCase(2.3493769750220914d, 242, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, 242, "diffcalc-test")]
[TestCase(2.797245912537965d, 242, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());

View File

@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
private readonly double[] holdEndTimes;
private readonly double[] startTimes;
private readonly double[] endTimes;
private readonly double[] individualStrains;
private double individualStrain;
@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
public Strain(Mod[] mods, int totalColumns)
: base(mods)
{
holdEndTimes = new double[totalColumns];
startTimes = new double[totalColumns];
endTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
overallStrain = 1;
}
@ -38,32 +40,27 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
double startTime = maniaCurrent.StartTime;
double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column;
double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
bool isOverlapping = false;
// Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i)
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
for (int i = 0; i < endTimes.Length; ++i)
{
// The current note is overlapped if a previous note or end is overlapping the current note body
isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1);
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1))
holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i]));
// Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
}
holdEndTimes[column] = endTime;
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
@ -77,12 +74,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
// Increase individual strain in own column
// Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column];
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
// Decay and increase overallStrain
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
overallStrain += (1 + holdAddition) * holdFactor;
// Update startTimes and endTimes arrays
startTimes[column] = startTime;
endTimes[column] = endTime;
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
return individualStrain + overallStrain - CurrentStrain;
}

View File

@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
private void updateBeatmap()
{
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
Beatmap.SaveState();
}
}
}

View File

@ -15,6 +15,7 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@ -311,7 +312,7 @@ namespace osu.Game.Rulesets.Mania
return Array.Empty<KeyBinding>();
}
public override string GetVariantName(int variant)
public override LocalisableString GetVariantName(int variant)
{
switch (getPlayfieldType(variant))
{
@ -356,7 +357,7 @@ namespace osu.Game.Rulesets.Mania
};
}
public override string GetDisplayNameForHitResult(HitResult result)
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{

View File

@ -2,6 +2,7 @@
// 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.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 0.9;
public override string Description => "No more tricky speed changes!";
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Dual Stages";
public override string Acronym => "DS";
public override string Description => @"Double the stages, double the fun!";
public override LocalisableString Description => @"Double the stages, double the fun!";
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModEasy : ModEasyWithExtraLives
{
public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Fade In";
public override string Acronym => "FI";
public override string Description => @"Keys appear out of nowhere!";
public override LocalisableString Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();

View File

@ -3,13 +3,14 @@
using System;
using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHidden : ManiaModPlayfieldCover
{
public override string Description => @"Keys fade out before you hit them!";
public override LocalisableString Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();

View File

@ -8,6 +8,7 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
public override string Description => @"Replaces all hold notes with normal notes.";
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "IN";
public override double ScoreMultiplier => 1;
public override string Description => "Hold the keys. To the beat.";
public override LocalisableString Description => "Hold the keys. To the beat.";
public override IconUsage? Icon => FontAwesome.Solid.YinYang;

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey1 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 1;
public override string Name => "One Key";
public override string Acronym => "1K";
public override string Description => @"Play with one key.";
public override LocalisableString Description => @"Play with one key.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey10 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 10;
public override string Name => "Ten Keys";
public override string Acronym => "10K";
public override string Description => @"Play with ten keys.";
public override LocalisableString Description => @"Play with ten keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey2 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 2;
public override string Name => "Two Keys";
public override string Acronym => "2K";
public override string Description => @"Play with two keys.";
public override LocalisableString Description => @"Play with two keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey3 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 3;
public override string Name => "Three Keys";
public override string Acronym => "3K";
public override string Description => @"Play with three keys.";
public override LocalisableString Description => @"Play with three keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey4 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 4;
public override string Name => "Four Keys";
public override string Acronym => "4K";
public override string Description => @"Play with four keys.";
public override LocalisableString Description => @"Play with four keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey5 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 5;
public override string Name => "Five Keys";
public override string Acronym => "5K";
public override string Description => @"Play with five keys.";
public override LocalisableString Description => @"Play with five keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey6 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 6;
public override string Name => "Six Keys";
public override string Acronym => "6K";
public override string Description => @"Play with six keys.";
public override LocalisableString Description => @"Play with six keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey7 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 7;
public override string Name => "Seven Keys";
public override string Acronym => "7K";
public override string Description => @"Play with seven keys.";
public override LocalisableString Description => @"Play with seven keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey8 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 8;
public override string Name => "Eight Keys";
public override string Acronym => "8K";
public override string Description => @"Play with eight keys.";
public override LocalisableString Description => @"Play with eight keys.";
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey9 : ManiaKeyMod
@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 9;
public override string Name => "Nine Keys";
public override string Acronym => "9K";
public override string Description => @"Play with nine keys.";
public override LocalisableString Description => @"Play with nine keys.";
}
}

View File

@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{
public override string Description => "Notes are flipped horizontally.";
public override LocalisableString Description => "Notes are flipped horizontally.";
public void ApplyToBeatmap(IBeatmap beatmap)
{

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModRandom : ModRandom, IApplicableToBeatmap
{
public override string Description => @"Shuffle around the keys!";
public override LocalisableString Description => @"Shuffle around the keys!";
public void ApplyToBeatmap(IBeatmap beatmap)
{

View File

@ -0,0 +1,229 @@
// 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.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneObjectMerging : TestSceneOsuEditor
{
[Test]
public void TestSimpleMerge()
{
HitCircle? circle1 = null;
HitCircle? circle2 = null;
AddStep("select first two circles", () =>
{
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
EditorClock.Seek(circle1.StartTime);
EditorBeatmap.SelectedHitObjects.Add(circle1);
EditorBeatmap.SelectedHitObjects.Add(circle2);
});
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle2.Position, pathType: null)));
AddStep("undo", () => Editor.Undo());
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2));
}
[Test]
public void TestMergeCircleSlider()
{
HitCircle? circle1 = null;
Slider? slider = null;
HitCircle? circle2 = null;
AddStep("select a circle, slider, circle", () =>
{
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime);
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime);
EditorClock.Seek(circle1.StartTime);
EditorBeatmap.SelectedHitObjects.Add(circle1);
EditorBeatmap.SelectedHitObjects.Add(slider);
EditorBeatmap.SelectedHitObjects.Add(circle2);
});
mergeSelection();
AddAssert("slider created", () =>
{
if (circle1 is null || circle2 is null || slider is null)
return false;
var controlPoints = slider.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2];
args[0] = (circle1.Position, PathType.Linear);
for (int i = 0; i < controlPoints.Count; i++)
{
args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type);
}
args[^1] = (circle2.Position, null);
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddStep("undo", () => Editor.Undo());
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
}
[Test]
public void TestMergeSliderSlider()
{
Slider? slider1 = null;
SliderPath? slider1Path = null;
Slider? slider2 = null;
AddStep("select two sliders", () =>
{
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
EditorClock.Seek(slider1.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider1);
EditorBeatmap.SelectedHitObjects.Add(slider2);
});
mergeSelection();
AddAssert("slider created", () =>
{
if (slider1 is null || slider2 is null || slider1Path is null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
var controlPoints2 = slider2.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
for (int i = 0; i < controlPoints1.Count - 1; i++)
{
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
}
for (int i = 0; i < controlPoints2.Count; i++)
{
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
}
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddAssert("merged slider matches first slider", () =>
{
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples)
&& mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
});
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path is null || slider2 is null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
});
}
[Test]
public void TestNonMerge()
{
HitCircle? circle1 = null;
HitCircle? circle2 = null;
Spinner? spinner = null;
AddStep("select first two circles and spinner", () =>
{
circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle);
circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1);
spinner = (Spinner)EditorBeatmap.HitObjects.First(h => h is Spinner);
EditorClock.Seek(spinner.StartTime);
EditorBeatmap.SelectedHitObjects.Add(circle1);
EditorBeatmap.SelectedHitObjects.Add(circle2);
EditorBeatmap.SelectedHitObjects.Add(spinner);
});
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle2.Position, pathType: null)));
AddAssert("samples exist", sliderSampleExist);
AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
}
private void mergeSelection()
{
AddStep("merge selection", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.M);
InputManager.ReleaseKey(Key.LShift);
InputManager.ReleaseKey(Key.LControl);
});
}
private bool sliderCreatedFor(params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
{
if (EditorBeatmap.SelectedHitObjects.Count != 1)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
int i = 0;
foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
{
var controlPoint = mergedSlider.Path.ControlPoints[i++];
if (!Precision.AlmostEquals(controlPoint.Position + mergedSlider.Position, pos) || controlPoint.Type != pathType)
return false;
}
return true;
}
private bool objectsRestored(params HitObject[] objects)
{
foreach (var hitObject in objects)
{
if (EditorBeatmap.HitObjects.Contains(hitObject))
return false;
}
return true;
}
private bool sliderSampleExist()
{
if (EditorBeatmap.SelectedHitObjects.Count != 1)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return mergedSlider.Samples[0] is not null;
}
}
}

View File

@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
item?.Action?.Value();
});

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
ControlPoints =
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(OsuPlayfield.BASE_SIZE * 2 / 5),
new PathControlPoint(OsuPlayfield.BASE_SIZE * 3 / 5)
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(136, 205)),
new PathControlPoint(new Vector2(-4, 226))
}
}
}));
@ -99,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("move mouse to new point location", () =>
{
var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
InputManager.MoveMouseTo((firstPiece.ScreenSpaceDrawQuad.Centre + secondPiece.ScreenSpaceDrawQuad.Centre) / 2);
var pos = slider.Path.PositionAt(0.25d) + slider.Position;
InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos));
});
AddStep("move slider end", () =>
{
@ -175,6 +176,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSliderSnapped(false);
}
[Test]
public void TestRotatingSliderRetainsPerfectControlPointType()
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("rotate 90 degrees ccw", () =>
{
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
selectionHandler.HandleRotation(-90);
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
}
[Test]
public void TestFlippingSliderDoesNotSnap()
{
@ -200,6 +218,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSliderSnapped(false);
}
[Test]
public void TestFlippingSliderRetainsPerfectControlPointType()
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("flip slider horizontally", () =>
{
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
selectionHandler.OnPressed(new KeyBindingPressEvent<GlobalAction>(InputManager.CurrentState, GlobalAction.EditorFlipVertically));
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
}
[Test]
public void TestReversingSliderDoesNotSnap()
{

View File

@ -0,0 +1,255 @@
// 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.UserInterface;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderSplitting : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
private Slider? slider;
private PathControlPointVisualiser? visualiser;
private const double split_gap = 100;
[Test]
public void TestBasicSplit()
{
double endTime = 0;
AddStep("add slider", () =>
{
slider = new Slider
{
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
};
EditorBeatmap.Add(slider);
endTime = slider.EndTime;
});
AddStep("select added slider", () =>
{
EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
});
moveMouseToControlPoint(2);
AddStep("select control point", () =>
{
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
});
addContextMenuItemStep("Split control point");
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 &&
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(150, 200), null),
(new Vector2(300, 50), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap,
(new Vector2(300, 50), PathType.PerfectCurve),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
));
AddStep("undo", () => Editor.Undo());
AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(150, 200), null),
(new Vector2(300, 50), PathType.PerfectCurve),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
));
}
[Test]
public void TestDoubleSplit()
{
double endTime = 0;
AddStep("add slider", () =>
{
slider = new Slider
{
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.Bezier),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150), PathType.Catmull),
new PathControlPoint(new Vector2(300, 200)),
new PathControlPoint(new Vector2(400, 250))
})
};
EditorBeatmap.Add(slider);
endTime = slider.EndTime;
});
AddStep("select added slider", () =>
{
EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
});
moveMouseToControlPoint(2);
AddStep("select first control point", () =>
{
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
});
moveMouseToControlPoint(4);
AddStep("select second control point", () =>
{
if (visualiser is not null) visualiser.Pieces[4].IsSelected.Value = true;
});
addContextMenuItemStep("Split 2 control points");
AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 &&
sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap,
(new Vector2(0, 50), PathType.PerfectCurve),
(new Vector2(150, 200), null),
(new Vector2(300, 50), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap,
(new Vector2(300, 50), PathType.Bezier),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2,
(new Vector2(400, 200), PathType.Catmull),
(new Vector2(300, 250), null),
(new Vector2(400, 300), null)
));
}
[Test]
public void TestSplitRetainsHitsounds()
{
HitSampleInfo? sample = null;
AddStep("add slider", () =>
{
slider = new Slider
{
Position = new Vector2(0, 50),
LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
};
EditorBeatmap.Add(slider);
});
AddStep("add hitsounds", () =>
{
if (slider is null) return;
slider.SampleControlPoint.SampleBank = "soft";
slider.SampleControlPoint.SampleVolume = 70;
sample = new HitSampleInfo("hitwhistle");
slider.Samples.Add(sample);
});
AddStep("select added slider", () =>
{
EditorBeatmap.SelectedHitObjects.Add(slider);
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
});
moveMouseToControlPoint(2);
AddStep("select control point", () =>
{
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
});
addContextMenuItemStep("Split control point");
AddAssert("sliders have hitsounds", hasHitsounds);
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]));
AddStep("remove first slider", () => EditorBeatmap.RemoveAt(0));
AddStep("undo", () => Editor.Undo());
AddAssert("sliders have hitsounds", hasHitsounds);
bool hasHitsounds() => sample is not null &&
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" &&
o.SampleControlPoint.SampleVolume == 70 &&
o.Samples.Contains(sample));
}
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)
{
if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false;
int i = 0;
foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints)
{
var controlPoint = s.Path.ControlPoints[i++];
if (!Precision.AlmostEquals(controlPoint.Position + s.Position, pos) || controlPoint.Type != pathType)
return false;
}
return true;
}
private void moveMouseToControlPoint(int index)
{
AddStep($"move mouse to control point {index}", () =>
{
if (slider is null || visualiser is null) return;
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
});
}
private void addContextMenuItemStep(string contextMenuText)
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
if (visualiser is null) return;
MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
item?.Action?.Value();
});
}
}
}

View File

@ -199,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
addSeekStep(1000);
AddAssert("progress almost same", () => expectedProgress, () => Is.EqualTo(drawableSpinner.Progress).Within(0.05));
AddAssert("spm almost same", () => expectedSpm, () => Is.EqualTo(drawableSpinner.SpinsPerMinute.Value).Within(2.0));
AddAssert("progress almost same", () => drawableSpinner.Progress, () => Is.EqualTo(expectedProgress).Within(0.05));
AddAssert("spm almost same", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(expectedSpm).Within(2.0));
}
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => time, () => Is.EqualTo(Player.DrawableRuleset.FrameStableClock.CurrentTime).Within(100));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
}
private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () =>

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -270,8 +270,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
// Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
// Clamp miss count to maximum amount of possible breaks
comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss);
return Math.Max(countMiss, comboBasedMissCount);
}

View File

@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private InputManager inputManager;
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
public Action<List<PathControlPoint>> SplitControlPointsRequested;
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
@ -104,6 +105,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return true;
}
private bool splitSelected()
{
List<PathControlPoint> controlPointsToSplitAt = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList();
// Ensure that there are any points to be split
if (controlPointsToSplitAt.Count == 0)
return false;
changeHandler?.BeginChange();
SplitControlPointsRequested?.Invoke(controlPointsToSplitAt);
changeHandler?.EndChange();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
private bool isSplittable(PathControlPointPiece p) =>
// A slider can only be split on control points which connect two different slider segments.
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
@ -142,8 +166,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Remove:
foreach (var point in e.OldItems.Cast<PathControlPoint>())
{
Pieces.RemoveAll(p => p.ControlPoint == point);
Connections.RemoveAll(c => c.ControlPoint == point);
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately();
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
connection.RemoveAndDisposeImmediately();
}
// If removing before the end of the path,
@ -322,25 +348,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (count == 0)
return null;
List<MenuItem> items = new List<MenuItem>();
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
int splittableCount = splittablePieces.Count;
List<MenuItem> curveTypeItems = new List<MenuItem>();
if (!selectedPieces.Contains(Pieces[0]))
items.Add(createMenuItemForPathType(null));
curveTypeItems.Add(createMenuItemForPathType(null));
// todo: hide/disable items which aren't valid for selected points
items.Add(createMenuItemForPathType(PathType.Linear));
items.Add(createMenuItemForPathType(PathType.PerfectCurve));
items.Add(createMenuItemForPathType(PathType.Bezier));
items.Add(createMenuItemForPathType(PathType.Catmull));
curveTypeItems.Add(createMenuItemForPathType(PathType.Linear));
curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve));
curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier));
curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull));
return new MenuItem[]
var menuItems = new List<MenuItem>
{
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()),
new OsuMenuItem("Curve type")
{
Items = items
Items = curveTypeItems
}
};
if (splittableCount > 0)
{
menuItems.Add(new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
MenuItemType.Destructive,
() => splitSelected()));
}
menuItems.Add(
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
MenuItemType.Destructive,
() => DeleteSelected())
);
return menuItems.ToArray();
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@ -111,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
{
RemoveControlPointsRequested = removeControlPoints
RemoveControlPointsRequested = removeControlPoints,
SplitControlPointsRequested = splitControlPoints
});
base.OnSelected();
@ -249,15 +251,83 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HitObject.Position += first;
}
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
{
// Arbitrary gap in milliseconds to put between split slider pieces
const double split_gap = 100;
// Ensure that there are any points to be split
if (controlPointsToSplitAt.Count == 0)
return;
editorBeatmap.SelectedHitObjects.Clear();
foreach (var splitPoint in controlPointsToSplitAt)
{
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
continue;
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
int index = controlPoints.IndexOf(splitPoint);
if (index <= 0)
continue;
// Extract the split portion and remove from the original slider.
var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index);
// Turn the control points which were split off into a new slider.
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
var newSlider = new Slider
{
StartTime = HitObject.StartTime,
Position = HitObject.Position + splitControlPoints[0].Position,
NewCombo = HitObject.NewCombo,
SampleControlPoint = samplePoint,
DifficultyControlPoint = difficultyPoint,
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
RepeatCount = HitObject.RepeatCount,
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray())
};
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
HitObject.StartTime += split_gap;
editorBeatmap.Add(newSlider);
HitObject.NewCombo = false;
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
HitObject.StartTime += newSlider.SpanDuration;
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
{
HitObject.Path.ExpectedDistance.Value = null;
}
}
// Once all required pieces have been split off, the original slider has the final split.
// As a final step, we must reset its control points to have an origin of (0,0).
Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints)
c.Position -= first;
HitObject.Position += first;
}
private void convertToStream()
{
if (editorBeatmap == null || changeHandler == null || beatDivisor == null)
if (editorBeatmap == null || beatDivisor == null)
return;
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
changeHandler.BeginChange();
changeHandler?.BeginChange();
int i = 0;
double time = HitObject.StartTime;
@ -292,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
editorBeatmap.Remove(HitObject);
changeHandler.EndChange();
changeHandler?.EndChange();
}
public override MenuItem[] ContextMenuItems => new MenuItem[]

View File

@ -6,8 +6,11 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -15,6 +18,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit
{
@ -53,6 +57,17 @@ namespace osu.Game.Rulesets.Osu.Edit
referencePathTypes = null;
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
{
mergeSelection();
return true;
}
return false;
}
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
var hitObjects = selectedMovableObjects;
@ -112,13 +127,16 @@ namespace osu.Game.Rulesets.Osu.Edit
{
didFlip = true;
foreach (var point in slider.Path.ControlPoints)
{
point.Position = new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * point.Position.X,
(direction == Direction.Vertical ? -1 : 1) * point.Position.Y
);
}
var controlPoints = slider.Path.ControlPoints.Select(p =>
new PathControlPoint(new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * p.Position.X,
(direction == Direction.Vertical ? -1 : 1) * p.Position.Y
), p.Type)).ToArray();
// Importantly, update as a single operation so automatic adjustment of control points to different
// curve types does not unexpectedly trigger and change the slider's shape.
slider.Path.ControlPoints.Clear();
slider.Path.ControlPoints.AddRange(controlPoints);
}
}
@ -168,8 +186,13 @@ namespace osu.Game.Rulesets.Osu.Edit
if (h is IHasPath path)
{
foreach (var point in path.Path.ControlPoints)
point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta);
var controlPoints = path.Path.ControlPoints.Select(p =>
new PathControlPoint(RotatePointAroundOrigin(p.Position, Vector2.Zero, delta), p.Type)).ToArray();
// Importantly, update as a single operation so automatic adjustment of control points to different
// curve types does not unexpectedly trigger and change the slider's shape.
path.Path.ControlPoints.Clear();
path.Path.ControlPoints.AddRange(controlPoints);
}
}
@ -320,7 +343,110 @@ namespace osu.Game.Rulesets.Osu.Edit
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.Where(h => h is not Spinner)
.ToArray();
/// <summary>
/// All osu! hitobjects which can be merged.
/// </summary>
private OsuHitObject[] selectedMergeableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => h is HitCircle or Slider)
.OrderBy(h => h.StartTime)
.ToArray();
private void mergeSelection()
{
var mergeableObjects = selectedMergeableObjects;
if (mergeableObjects.Length < 2)
return;
ChangeHandler?.BeginChange();
// Have an initial slider object.
var firstHitObject = mergeableObjects[0];
var mergedHitObject = firstHitObject as Slider ?? new Slider
{
StartTime = firstHitObject.StartTime,
Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo,
SampleControlPoint = firstHitObject.SampleControlPoint,
Samples = firstHitObject.Samples,
};
if (mergedHitObject.Path.ControlPoints.Count == 0)
{
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear));
}
// Merge all the selected hit objects into one slider path.
bool lastCircle = firstHitObject is HitCircle;
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
{
if (selectedMergeableObject is IHasPath hasPath)
{
var offset = lastCircle ? selectedMergeableObject.Position - mergedHitObject.Position : mergedHitObject.Path.ControlPoints[^1].Position;
float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
// Calculate the distance required to travel to the expected distance of the merging slider.
mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance;
// Remove the last control point if it sits exactly on the start of the next control point.
if (Precision.AlmostEquals(distanceToLastControlPoint, 0))
{
mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1);
}
mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type)));
lastCircle = false;
}
else
{
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
if (!lastCircle)
{
mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear;
}
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));
mergedHitObject.Path.ExpectedDistance.Value = null;
lastCircle = true;
}
}
// Make sure only the merged hit object is in the beatmap.
if (firstHitObject is Slider)
{
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
{
EditorBeatmap.Remove(selectedMergeableObject);
}
}
else
{
foreach (var selectedMergeableObject in mergeableObjects)
{
EditorBeatmap.Remove(selectedMergeableObject);
}
EditorBeatmap.Add(mergedHitObject);
}
// Make sure the merged hitobject is selected.
SelectedItems.Clear();
SelectedItems.Add(mergedHitObject);
ChangeHandler?.EndChange();
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
{
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;
if (selectedMergeableObjects.Length > 1)
yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection);
}
}
}

View File

@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
private void updateBeatmap()
{
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
Beatmap.SaveState();
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
public override LocalisableString Description => @"Don't use the same key twice in a row!";
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
public override string Description => "Never trust the approach circles...";
public override LocalisableString Description => "Never trust the approach circles...";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.StateChanges;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "AP";
public override IconUsage? Icon => OsuIcon.ModAutopilot;
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
@ -30,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager = null!;
private IFrameStableClock gameplayClock = null!;
private List<OsuReplayFrame> replayFrames = null!;
private int currentFrame;
@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (currentFrame == replayFrames.Count - 1) return;
double time = gameplayClock.CurrentTime;
double time = playfield.Clock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
@ -55,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
gameplayClock = drawableRuleset.FrameStableClock;
// Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModBlinds : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
{
public override string Name => "Blinds";
public override string Description => "Play with blinds on your screen.";
public override LocalisableString Description => "Play with blinds on your screen.";
public override string Acronym => "BL";
public override IconUsage? Icon => FontAwesome.Solid.Adjust;

View File

@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt;
public override string Description => "Hit them at the right size!";
public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModEasy : ModEasyWithExtraLives
{
public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV;
public override string Description => "Hit them at the right size!";
public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat

View File

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")]
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };

View File

@ -4,6 +4,8 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@ -22,12 +24,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles your cursor is a magnet!";
public override LocalisableString Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock = null!;
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{
@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
gameplayClock = drawableRuleset.FrameStableClock;
// Hide judgment displays and 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.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
@ -55,27 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
easeTo(circle, cursorPos);
easeTo(playfield.Clock, circle, cursorPos);
break;
case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, cursorPos);
easeTo(playfield.Clock, slider, cursorPos);
else
easeTo(slider, cursorPos - slider.Ball.DrawPosition);
easeTo(playfield.Clock, slider, cursorPos - slider.Ball.DrawPosition);
break;
}
}
}
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination)
{
double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y);
}

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModMirror : ModMirror, IApplicableToHitObject
{
public override string Description => "Flip objects on the chosen axes.";
public override LocalisableString Description => "Flip objects on the chosen axes.";
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap
{
public override string Description => "Where's the cursor?";
public override LocalisableString Description => "Where's the cursor?";
private PeriodTracker spinnerPeriods = null!;

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// </summary>
public class OsuModRandom : ModRandom, IApplicableToBeatmap
{
public override string Description => "It never gets boring!";
public override LocalisableString Description => "It never gets boring!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
/// <summary>

View File

@ -2,8 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@ -22,12 +23,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => "Repel";
public override string Acronym => "RP";
public override ModType Type => ModType.Fun;
public override string Description => "Hit objects run away!";
public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
private IFrameStableClock? gameplayClock;
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
{
@ -38,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
gameplayClock = drawableRuleset.FrameStableClock;
// Hide judgment displays and 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.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
@ -68,29 +65,27 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
easeTo(circle, destination, cursorPos);
easeTo(playfield.Clock, circle, destination, cursorPos);
break;
case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, destination, cursorPos);
easeTo(playfield.Clock, slider, destination, cursorPos);
else
easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos);
easeTo(playfield.Clock, slider, destination - slider.Ball.DrawPosition, cursorPos);
break;
}
}
}
private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
{
Debug.Assert(gameplayClock != null);
double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y);
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => @"Single Tap";
public override string Acronym => @"SG";
public override string Description => @"You must only use one key!";
public override LocalisableString Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;

View File

@ -4,6 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "SI";
public override IconUsage? Icon => FontAwesome.Solid.Undo;
public override ModType Type => ModType.Fun;
public override string Description => "Circles spin in. No approach circles.";
public override LocalisableString Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1;
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,

View File

@ -4,6 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunOut;
public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed.";
public override LocalisableString Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => @"Strict Tracking";
public override string Acronym => @"ST";
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Once you start a slider, follow precisely or get a miss.";
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "TP";
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => OsuIcon.ModTarget;
public override string Description => @"Practice keeping up with the beat of the song.";
public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
public override string Description => "Automatically applied to plays on devices with a touchscreen.";
public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;

View File

@ -4,6 +4,7 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => "Traceable";
public override string Acronym => "TC";
public override ModType Type => ModType.Fun;
public override string 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 Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };

View File

@ -4,6 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "TR";
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAlt;
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "WG";
public override IconUsage? Icon => FontAwesome.Solid.Certificate;
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override LocalisableString Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };

View File

@ -32,6 +32,7 @@ using osu.Game.Skinning;
using System;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.Edit.Setup;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
@ -253,7 +254,7 @@ namespace osu.Game.Rulesets.Osu
};
}
public override string GetDisplayNameForHitResult(HitResult result)
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osuTK;
@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
protected override string Message => "Click the orange cursor to resume";
protected override LocalisableString Message => "Click the orange cursor to resume";
[BackgroundDependencyLoader]
private void load()

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using Humanizer;
@ -36,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 },
};
private TaikoScoreProcessor scoreProcessor;
private TaikoScoreProcessor scoreProcessor = null!;
private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>();
@ -65,6 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestInitialState()
{
AddStep("set beatmap", () => setBeatmap());
AddStep("create mascot", () => SetContents(_ => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle));
@ -89,9 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestIdleState()
{
AddStep("set beatmap", () => setBeatmap());
createDrawableRuleset();
prepareDrawableRulesetAndBeatmap(false);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
@ -100,9 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestKiaiState()
{
AddStep("set beatmap", () => setBeatmap(true));
createDrawableRuleset();
prepareDrawableRulesetAndBeatmap(true);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai);
@ -112,9 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestMissState()
{
AddStep("set beatmap", () => setBeatmap());
createDrawableRuleset();
prepareDrawableRulesetAndBeatmap(false);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
@ -126,9 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestCase(false)]
public void TestClearStateOnComboMilestone(bool kiai)
{
AddStep("set beatmap", () => setBeatmap(kiai));
createDrawableRuleset();
prepareDrawableRulesetAndBeatmap(kiai);
AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49);
@ -139,9 +131,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestCase(false, TaikoMascotAnimationState.Idle)]
public void TestClearStateOnClearedSwell(bool kiai, TaikoMascotAnimationState expectedStateAfterClear)
{
AddStep("set beatmap", () => setBeatmap(kiai));
createDrawableRuleset();
prepareDrawableRulesetAndBeatmap(kiai);
assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear);
AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLowerInvariant()}", () => allMascotsIn(expectedStateAfterClear));
@ -175,25 +165,27 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
scoreProcessor.ApplyBeatmap(Beatmap.Value.Beatmap);
}
private void createDrawableRuleset()
private void prepareDrawableRulesetAndBeatmap(bool kiai)
{
AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded);
AddStep("set beatmap", () => setBeatmap(kiai));
AddStep("create drawable ruleset", () =>
{
Beatmap.Value.Track.Start();
SetContents(_ =>
{
var ruleset = new TaikoRuleset();
return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo));
});
});
AddUntilStep("wait for track to be loaded", () => MusicController.TrackLoaded);
AddStep("start track", () => MusicController.CurrentTrack.Restart());
AddUntilStep("wait for track started", () => MusicController.IsPlaying);
}
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
{
TaikoMascotAnimationState[] mascotStates = null;
TaikoMascotAnimationState[] mascotStates = null!;
AddStep($"{judgementResult.Type.ToString().ToLowerInvariant()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
() =>
@ -204,7 +196,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
});
AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.All(state => state == expectedState));
AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.Distinct(), () => Is.EquivalentTo(new[] { expectedState }));
}
private void applyNewResult(JudgementResult judgementResult)

View File

@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(1.9971301024093662d, 200, "diffcalc-test")]
[TestCase(1.9971301024093662d, 200, "diffcalc-test-strong")]
[TestCase(3.1098944660126882d, 200, "diffcalc-test")]
[TestCase(3.1098944660126882d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(3.1645810961313674d, 200, "diffcalc-test")]
[TestCase(3.1645810961313674d, 200, "diffcalc-test-strong")]
[TestCase(4.0974106752474251d, 200, "diffcalc-test")]
[TestCase(4.0974106752474251d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());

View File

@ -0,0 +1,67 @@
// 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.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class ColourEvaluator
{
/// <summary>
/// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
/// </summary>
/// <param name="val">The input value.</param>
/// <param name="center">The center of the sigmoid, where the largest gradient occurs and value is equal to middle.</param>
/// <param name="width">The radius of the sigmoid, outside of which values are near the minimum/maximum.</param>
/// <param name="middle">The middle of the sigmoid output.</param>
/// <param name="height">The height of the sigmoid output. This will be equal to max value - min value.</param>
private static double sigmoid(double val, double center, double width, double middle, double height)
{
double sigmoid = Math.Tanh(Math.E * -(val - center) / width);
return sigmoid * (height / 2) + middle;
}
/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
/// </summary>
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
{
return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
}
/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="AlternatingMonoPattern"/>.
/// </summary>
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
{
return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
}
/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="RepeatingHitPatterns"/>.
/// </summary>
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
{
return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1));
}
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
{
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
double difficulty = 0.0d;
if (colour.MonoStreak != null) // Difficulty for MonoStreak
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
if (colour.RepeatingHitPattern != null) // Difficulty for RepeatingHitPattern
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
return difficulty;
}
}
}

View File

@ -0,0 +1,53 @@
// 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.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class StaminaEvaluator
{
/// <summary>
/// Applies a speed bonus dependent on the time since the last hit performed using this key.
/// </summary>
/// <param name="interval">The interval between the current and previous note hit using the same key.</param>
private static double speedBonus(double interval)
{
// Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
// Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
// TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
interval = Math.Max(interval, 50);
return 30 / interval;
}
/// <summary>
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
/// maximum possible interval between two hits using the same key, by alternating 2 keys for each colour.
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
if (current.BaseObject is not Hit)
{
return 0.0;
}
// Find the previous hit object hit by the current key, which is two notes of the same colour prior.
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(1);
if (keyPrevious == null)
{
// There is no previous hit object hit by the current key
return 0.0;
}
double objectStrain = 0.5; // Add a base strain to all objects
objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime);
return objectStrain;
}
}
}

View File

@ -0,0 +1,53 @@
// 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;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
{
/// <summary>
/// Encodes a list of <see cref="MonoStreak"/>s.
/// <see cref="MonoStreak"/>s with the same <see cref="MonoStreak.RunLength"/> are grouped together.
/// </summary>
public class AlternatingMonoPattern
{
/// <summary>
/// <see cref="MonoStreak"/>s that are grouped together within this <see cref="AlternatingMonoPattern"/>.
/// </summary>
public readonly List<MonoStreak> MonoStreaks = new List<MonoStreak>();
/// <summary>
/// The parent <see cref="RepeatingHitPatterns"/> that contains this <see cref="AlternatingMonoPattern"/>
/// </summary>
public RepeatingHitPatterns Parent = null!;
/// <summary>
/// Index of this <see cref="AlternatingMonoPattern"/> within it's parent <see cref="RepeatingHitPatterns"/>
/// </summary>
public int Index;
/// <summary>
/// The first <see cref="TaikoDifficultyHitObject"/> in this <see cref="AlternatingMonoPattern"/>.
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => MonoStreaks[0].FirstHitObject;
/// <summary>
/// Determine if this <see cref="AlternatingMonoPattern"/> is a repetition of another <see cref="AlternatingMonoPattern"/>. This
/// is a strict comparison and is true if and only if the colour sequence is exactly the same.
/// </summary>
public bool IsRepetitionOf(AlternatingMonoPattern other)
{
return HasIdenticalMonoLength(other) &&
other.MonoStreaks.Count == MonoStreaks.Count &&
other.MonoStreaks[0].HitType == MonoStreaks[0].HitType;
}
/// <summary>
/// Determine if this <see cref="AlternatingMonoPattern"/> has the same mono length of another <see cref="AlternatingMonoPattern"/>.
/// </summary>
public bool HasIdenticalMonoLength(AlternatingMonoPattern other)
{
return other.MonoStreaks[0].RunLength == MonoStreaks[0].RunLength;
}
}
}

View 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 osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
{
/// <summary>
/// Encode colour information for a sequence of <see cref="TaikoDifficultyHitObject"/>s. Consecutive <see cref="TaikoDifficultyHitObject"/>s
/// of the same <see cref="Objects.HitType"/> are encoded within the same <see cref="MonoStreak"/>.
/// </summary>
public class MonoStreak
{
/// <summary>
/// List of <see cref="DifficultyHitObject"/>s that are encoded within this <see cref="MonoStreak"/>.
/// </summary>
public List<TaikoDifficultyHitObject> HitObjects { get; private set; } = new List<TaikoDifficultyHitObject>();
/// <summary>
/// The parent <see cref="AlternatingMonoPattern"/> that contains this <see cref="MonoStreak"/>
/// </summary>
public AlternatingMonoPattern Parent = null!;
/// <summary>
/// Index of this <see cref="MonoStreak"/> within it's parent <see cref="AlternatingMonoPattern"/>
/// </summary>
public int Index;
/// <summary>
/// The first <see cref="TaikoDifficultyHitObject"/> in this <see cref="MonoStreak"/>.
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
/// <summary>
/// The hit type of all objects encoded within this <see cref="MonoStreak"/>
/// </summary>
public HitType? HitType => (HitObjects[0].BaseObject as Hit)?.Type;
/// <summary>
/// How long the mono pattern encoded within is
/// </summary>
public int RunLength => HitObjects.Count;
}
}

View File

@ -0,0 +1,94 @@
// 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;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
{
/// <summary>
/// Encodes a list of <see cref="AlternatingMonoPattern"/>s, grouped together by back and forth repetition of the same
/// <see cref="AlternatingMonoPattern"/>. Also stores the repetition interval between this and the previous <see cref="RepeatingHitPatterns"/>.
/// </summary>
public class RepeatingHitPatterns
{
/// <summary>
/// Maximum amount of <see cref="RepeatingHitPatterns"/>s to look back to find a repetition.
/// </summary>
private const int max_repetition_interval = 16;
/// <summary>
/// The <see cref="AlternatingMonoPattern"/>s that are grouped together within this <see cref="RepeatingHitPatterns"/>.
/// </summary>
public readonly List<AlternatingMonoPattern> AlternatingMonoPatterns = new List<AlternatingMonoPattern>();
/// <summary>
/// The parent <see cref="TaikoDifficultyHitObject"/> in this <see cref="RepeatingHitPatterns"/>
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject;
/// <summary>
/// The previous <see cref="RepeatingHitPatterns"/>. This is used to determine the repetition interval.
/// </summary>
public readonly RepeatingHitPatterns? Previous;
/// <summary>
/// How many <see cref="RepeatingHitPatterns"/> between the current and previous identical <see cref="RepeatingHitPatterns"/>.
/// If no repetition is found this will have a value of <see cref="max_repetition_interval"/> + 1.
/// </summary>
public int RepetitionInterval { get; private set; } = max_repetition_interval + 1;
public RepeatingHitPatterns(RepeatingHitPatterns? previous)
{
Previous = previous;
}
/// <summary>
/// Returns true if other is considered a repetition of this pattern. This is true if other's first two payloads
/// have identical mono lengths.
/// </summary>
private bool isRepetitionOf(RepeatingHitPatterns other)
{
if (AlternatingMonoPatterns.Count != other.AlternatingMonoPatterns.Count) return false;
for (int i = 0; i < Math.Min(AlternatingMonoPatterns.Count, 2); i++)
{
if (!AlternatingMonoPatterns[i].HasIdenticalMonoLength(other.AlternatingMonoPatterns[i])) return false;
}
return true;
}
/// <summary>
/// Finds the closest previous <see cref="RepeatingHitPatterns"/> that has the identical <see cref="AlternatingMonoPatterns"/>.
/// Interval is defined as the amount of <see cref="RepeatingHitPatterns"/> chunks between the current and repeated patterns.
/// </summary>
public void FindRepetitionInterval()
{
if (Previous == null)
{
RepetitionInterval = max_repetition_interval + 1;
return;
}
RepeatingHitPatterns? other = Previous;
int interval = 1;
while (interval < max_repetition_interval)
{
if (isRepetitionOf(other))
{
RepetitionInterval = Math.Min(interval, max_repetition_interval);
return;
}
other = other.Previous;
if (other == null) break;
++interval;
}
RepetitionInterval = max_repetition_interval + 1;
}
}
}

View File

@ -0,0 +1,167 @@
// 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.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
{
/// <summary>
/// Utility class to perform various encodings.
/// </summary>
public static class TaikoColourDifficultyPreprocessor
{
/// <summary>
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoDifficultyHitObjectColour"/>s,
/// assigning the appropriate <see cref="TaikoDifficultyHitObjectColour"/>s to each <see cref="TaikoDifficultyHitObject"/>,
/// and pre-evaluating colour difficulty of each <see cref="TaikoDifficultyHitObject"/>.
/// </summary>
public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
{
List<RepeatingHitPatterns> hitPatterns = encode(hitObjects);
// Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is
// assigned with the relevant encodings.
foreach (var repeatingHitPattern in hitPatterns)
{
repeatingHitPattern.FirstHitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
// The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to
// keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with
// documentation.
for (int i = 0; i < repeatingHitPattern.AlternatingMonoPatterns.Count; ++i)
{
AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i];
monoPattern.Parent = repeatingHitPattern;
monoPattern.Index = i;
monoPattern.FirstHitObject.Colour.AlternatingMonoPattern = monoPattern;
for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j)
{
MonoStreak monoStreak = monoPattern.MonoStreaks[j];
monoStreak.Parent = monoPattern;
monoStreak.Index = j;
monoStreak.FirstHitObject.Colour.MonoStreak = monoStreak;
}
}
}
}
/// <summary>
/// Encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="RepeatingHitPatterns"/>s.
/// </summary>
private static List<RepeatingHitPatterns> encode(List<DifficultyHitObject> data)
{
List<MonoStreak> monoStreaks = encodeMonoStreak(data);
List<AlternatingMonoPattern> alternatingMonoPatterns = encodeAlternatingMonoPattern(monoStreaks);
List<RepeatingHitPatterns> repeatingHitPatterns = encodeRepeatingHitPattern(alternatingMonoPatterns);
return repeatingHitPatterns;
}
/// <summary>
/// Encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="MonoStreak"/>s.
/// </summary>
private static List<MonoStreak> encodeMonoStreak(List<DifficultyHitObject> data)
{
List<MonoStreak> monoStreaks = new List<MonoStreak>();
MonoStreak? currentMonoStreak = null;
for (int i = 0; i < data.Count; i++)
{
TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i];
// This ignores all non-note objects, which may or may not be the desired behaviour
TaikoDifficultyHitObject? previousObject = taikoObject.PreviousNote(0);
// If this is the first object in the list or the colour changed, create a new mono streak
if (currentMonoStreak == null || previousObject == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type)
{
currentMonoStreak = new MonoStreak();
monoStreaks.Add(currentMonoStreak);
}
// Add the current object to the encoded payload.
currentMonoStreak.HitObjects.Add(taikoObject);
}
return monoStreaks;
}
/// <summary>
/// Encodes a list of <see cref="MonoStreak"/>s into a list of <see cref="AlternatingMonoPattern"/>s.
/// </summary>
private static List<AlternatingMonoPattern> encodeAlternatingMonoPattern(List<MonoStreak> data)
{
List<AlternatingMonoPattern> monoPatterns = new List<AlternatingMonoPattern>();
AlternatingMonoPattern? currentMonoPattern = null;
for (int i = 0; i < data.Count; i++)
{
// Start a new AlternatingMonoPattern if the previous MonoStreak has a different mono length, or if this is the first MonoStreak in the list.
if (currentMonoPattern == null || data[i].RunLength != data[i - 1].RunLength)
{
currentMonoPattern = new AlternatingMonoPattern();
monoPatterns.Add(currentMonoPattern);
}
// Add the current MonoStreak to the encoded payload.
currentMonoPattern.MonoStreaks.Add(data[i]);
}
return monoPatterns;
}
/// <summary>
/// Encodes a list of <see cref="AlternatingMonoPattern"/>s into a list of <see cref="RepeatingHitPatterns"/>s.
/// </summary>
private static List<RepeatingHitPatterns> encodeRepeatingHitPattern(List<AlternatingMonoPattern> data)
{
List<RepeatingHitPatterns> hitPatterns = new List<RepeatingHitPatterns>();
RepeatingHitPatterns? currentHitPattern = null;
for (int i = 0; i < data.Count; i++)
{
// Start a new RepeatingHitPattern. AlternatingMonoPatterns that should be grouped together will be handled later within this loop.
currentHitPattern = new RepeatingHitPatterns(currentHitPattern);
// Determine if future AlternatingMonoPatterns should be grouped.
bool isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]);
if (!isCoupled)
{
// If not, add the current AlternatingMonoPattern to the encoded payload and continue.
currentHitPattern.AlternatingMonoPatterns.Add(data[i]);
}
else
{
// If so, add the current AlternatingMonoPattern to the encoded payload and start repeatedly checking if the
// subsequent AlternatingMonoPatterns should be grouped by increasing i and doing the appropriate isCoupled check.
while (isCoupled)
{
currentHitPattern.AlternatingMonoPatterns.Add(data[i]);
i++;
isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]);
}
// Skip over viewed data and add the rest to the payload
currentHitPattern.AlternatingMonoPatterns.Add(data[i]);
currentHitPattern.AlternatingMonoPatterns.Add(data[i + 1]);
i++;
}
hitPatterns.Add(currentHitPattern);
}
// Final pass to find repetition intervals
for (int i = 0; i < hitPatterns.Count; i++)
{
hitPatterns[i].FindRepetitionInterval();
}
return hitPatterns;
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
{
/// <summary>
/// Stores colour compression information for a <see cref="TaikoDifficultyHitObject"/>.
/// </summary>
public class TaikoDifficultyHitObjectColour
{
/// <summary>
/// The <see cref="MonoStreak"/> that encodes this note, only present if this is the first note within a <see cref="MonoStreak"/>
/// </summary>
public MonoStreak? MonoStreak;
/// <summary>
/// The <see cref="AlternatingMonoPattern"/> that encodes this note, only present if this is the first note within a <see cref="AlternatingMonoPattern"/>
/// </summary>
public AlternatingMonoPattern? AlternatingMonoPattern;
/// <summary>
/// The <see cref="RepeatingHitPattern"/> that encodes this note, only present if this is the first note within a <see cref="RepeatingHitPattern"/>
/// </summary>
public RepeatingHitPatterns? RepeatingHitPattern;
}
}

View File

@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
/// <summary>
/// Represents a rhythm change in a taiko map.

View File

@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
@ -17,21 +17,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// </summary>
public class TaikoDifficultyHitObject : DifficultyHitObject
{
/// <summary>
/// The list of all <see cref="TaikoDifficultyHitObject"/> of the same colour as this <see cref="TaikoDifficultyHitObject"/> in the beatmap.
/// </summary>
private readonly IReadOnlyList<TaikoDifficultyHitObject>? monoDifficultyHitObjects;
/// <summary>
/// The index of this <see cref="TaikoDifficultyHitObject"/> in <see cref="monoDifficultyHitObjects"/>.
/// </summary>
public readonly int MonoIndex;
/// <summary>
/// The list of all <see cref="TaikoDifficultyHitObject"/> that is either a regular note or finisher in the beatmap
/// </summary>
private readonly IReadOnlyList<TaikoDifficultyHitObject> noteDifficultyHitObjects;
/// <summary>
/// The index of this <see cref="TaikoDifficultyHitObject"/> in <see cref="noteDifficultyHitObjects"/>.
/// </summary>
public readonly int NoteIndex;
/// <summary>
/// The rhythm required to hit this hit object.
/// </summary>
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
/// <summary>
/// The hit type of this hit object.
/// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used
/// by other skills in the future.
/// </summary>
public readonly HitType? HitType;
/// <summary>
/// Whether the object should carry a penalty due to being hittable using special techniques
/// making it easier to do so.
/// </summary>
public bool StaminaCheese;
public readonly TaikoDifficultyHitObjectColour Colour;
/// <summary>
/// Creates a new difficulty hit object.
@ -40,15 +55,44 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</param>
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
/// <param name="clockRate">The rate of the gameplay clock. Modified by speed-changing mods.</param>
/// <param name="objects">The list of <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
/// /// <param name="index">The position of this <see cref="DifficultyHitObject"/> in the <paramref name="objects"/> list.</param>
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List<DifficultyHitObject> objects, int index)
/// <param name="objects">The list of all <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
/// <param name="centreHitObjects">The list of centre (don) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
/// <param name="rimHitObjects">The list of rim (kat) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
/// <param name="noteObjects">The list of <see cref="DifficultyHitObject"/>s that is a hit (i.e. not a drumroll or swell) in the current beatmap.</param>
/// <param name="index">The position of this <see cref="DifficultyHitObject"/> in the <paramref name="objects"/> list.</param>
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate,
List<DifficultyHitObject> objects,
List<TaikoDifficultyHitObject> centreHitObjects,
List<TaikoDifficultyHitObject> rimHitObjects,
List<TaikoDifficultyHitObject> noteObjects, int index)
: base(hitObject, lastObject, clockRate, objects, index)
{
var currentHit = hitObject as Hit;
noteDifficultyHitObjects = noteObjects;
// Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
Colour = new TaikoDifficultyHitObjectColour();
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
HitType = currentHit?.Type;
switch ((hitObject as Hit)?.Type)
{
case HitType.Centre:
MonoIndex = centreHitObjects.Count;
centreHitObjects.Add(this);
monoDifficultyHitObjects = centreHitObjects;
break;
case HitType.Rim:
MonoIndex = rimHitObjects.Count;
rimHitObjects.Add(this);
monoDifficultyHitObjects = rimHitObjects;
break;
}
if (hitObject is Hit)
{
NoteIndex = noteObjects.Count;
noteObjects.Add(this);
}
}
/// <summary>
@ -87,5 +131,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
}
public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1));
public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1));
public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1));
public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1));
}
}

View File

@ -1,15 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
@ -18,29 +13,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// </summary>
public class Colour : StrainDecaySkill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
protected override double SkillMultiplier => 0.12;
/// <summary>
/// Maximum number of entries to keep in <see cref="monoHistory"/>.
/// </summary>
private const int mono_history_max_length = 5;
/// <summary>
/// Queue with the lengths of the last <see cref="mono_history_max_length"/> most recent mono (single-colour) patterns,
/// with the most recent value at the end of the queue.
/// </summary>
private readonly LimitedCapacityQueue<int> monoHistory = new LimitedCapacityQueue<int>(mono_history_max_length);
/// <summary>
/// The <see cref="HitType"/> of the last object hit before the one being considered.
/// </summary>
private HitType? previousHitType;
/// <summary>
/// Length of the current mono pattern.
/// </summary>
private int currentMonoLength;
// This is set to decay slower than other skills, due to the fact that only the first note of each encoding class
// having any difficulty values, and we want to allow colour difficulty to be able to build up even on
// slower maps.
protected override double StrainDecayBase => 0.8;
public Colour(Mod[] mods)
: base(mods)
@ -49,95 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
// changing from/to a drum roll or a swell does not constitute a colour change.
// hits spaced more than a second apart are also exempt from colour strain.
if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000))
{
monoHistory.Clear();
var currentHit = current.BaseObject as Hit;
currentMonoLength = currentHit != null ? 1 : 0;
previousHitType = currentHit?.Type;
return 0.0;
}
var taikoCurrent = (TaikoDifficultyHitObject)current;
double objectStrain = 0.0;
if (previousHitType != null && taikoCurrent.HitType != previousHitType)
{
// The colour has changed.
objectStrain = 1.0;
if (monoHistory.Count < 2)
{
// There needs to be at least two streaks to determine a strain.
objectStrain = 0.0;
}
else if ((monoHistory[^1] + currentMonoLength) % 2 == 0)
{
// The last streak in the history is guaranteed to be a different type to the current streak.
// If the total number of notes in the two streaks is even, nullify this object's strain.
objectStrain = 0.0;
}
objectStrain *= repetitionPenalties();
currentMonoLength = 1;
}
else
{
currentMonoLength += 1;
}
previousHitType = taikoCurrent.HitType;
return objectStrain;
return ColourEvaluator.EvaluateDifficultyOf(current);
}
/// <summary>
/// The penalty to apply due to the length of repetition in colour streaks.
/// </summary>
private double repetitionPenalties()
{
const int most_recent_patterns_to_compare = 2;
double penalty = 1.0;
monoHistory.Enqueue(currentMonoLength);
for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--)
{
if (!isSamePattern(start, most_recent_patterns_to_compare))
continue;
int notesSince = 0;
for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i];
penalty *= repetitionPenalty(notesSince);
break;
}
return penalty;
}
/// <summary>
/// Determines whether the last <paramref name="mostRecentPatternsToCompare"/> patterns have repeated in the history
/// of single-colour note sequences, starting from <paramref name="start"/>.
/// </summary>
private bool isSamePattern(int start, int mostRecentPatternsToCompare)
{
for (int i = 0; i < mostRecentPatternsToCompare; i++)
{
if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i])
return false;
}
return true;
}
/// <summary>
/// Calculates the strain penalty for a colour pattern repetition.
/// </summary>
/// <param name="notesSince">The number of notes since the last repetition of the pattern.</param>
private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
}
}

View File

@ -0,0 +1,93 @@
// 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.OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
}
}

View File

@ -1,44 +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.
#nullable disable
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Stamina of a single key, calculated based on repetition speed.
/// </summary>
public class SingleKeyStamina
{
private double? previousHitTime;
/// <summary>
/// Similar to <see cref="StrainDecaySkill.StrainValueOf"/>
/// </summary>
public double StrainValueOf(DifficultyHitObject current)
{
if (previousHitTime == null)
{
previousHitTime = current.StartTime;
return 0;
}
double objectStrain = 0.5;
objectStrain += speedBonus(current.StartTime - previousHitTime.Value);
previousHitTime = current.StartTime;
return objectStrain;
}
/// <summary>
/// Applies a speed bonus dependent on the time since the last hit performed using this key.
/// </summary>
/// <param name="notePairDuration">The duration between the current and previous note hit using the same key.</param>
private double speedBonus(double notePairDuration)
{
return 175 / (notePairDuration + 100);
}
}
}

View File

@ -6,8 +6,7 @@
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
@ -19,31 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// </remarks>
public class Stamina : StrainDecaySkill
{
protected override double SkillMultiplier => 1;
protected override double SkillMultiplier => 1.1;
protected override double StrainDecayBase => 0.4;
private readonly SingleKeyStamina[] centreKeyStamina =
{
new SingleKeyStamina(),
new SingleKeyStamina()
};
private readonly SingleKeyStamina[] rimKeyStamina =
{
new SingleKeyStamina(),
new SingleKeyStamina()
};
/// <summary>
/// Current index into <see cref="centreKeyStamina" /> for a centre hit.
/// </summary>
private int centreKeyIndex;
/// <summary>
/// Current index into <see cref="rimKeyStamina" /> for a rim hit.
/// </summary>
private int rimKeyIndex;
/// <summary>
/// Creates a <see cref="Stamina"/> skill.
/// </summary>
@ -53,32 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
}
/// <summary>
/// Get the next <see cref="SingleKeyStamina"/> to use for the given <see cref="TaikoDifficultyHitObject"/>.
/// </summary>
/// <param name="current">The current <see cref="TaikoDifficultyHitObject"/>.</param>
private SingleKeyStamina getNextSingleKeyStamina(TaikoDifficultyHitObject current)
{
// Alternate key for the same color.
if (current.HitType == HitType.Centre)
{
centreKeyIndex = (centreKeyIndex + 1) % 2;
return centreKeyStamina[centreKeyIndex];
}
rimKeyIndex = (rimKeyIndex + 1) % 2;
return rimKeyStamina[rimKeyIndex];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
if (!(current.BaseObject is Hit))
{
return 0.0;
}
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
return getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject);
return StaminaEvaluator.EvaluateDifficultyOf(current);
}
}
}

View File

@ -29,13 +29,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public double ColourDifficulty { get; set; }
/// <summary>
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
/// The difficulty corresponding to the hardest parts of the map.
/// </summary>
/// <remarks>
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
/// </remarks>
[JsonProperty("approach_rate")]
public double ApproachRate { get; set; }
[JsonProperty("peak_difficulty")]
public double PeakDifficulty { get; set; }
/// <summary>
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
@ -22,9 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyCalculator : DifficultyCalculator
{
private const double rhythm_skill_multiplier = 0.014;
private const double colour_skill_multiplier = 0.01;
private const double stamina_skill_multiplier = 0.021;
private const double difficulty_multiplier = 1.35;
public override int Version => 20220701;
@ -33,12 +32,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
new Colour(mods),
new Rhythm(mods),
new Stamina(mods)
};
return new Skill[]
{
new Peaks(mods)
};
}
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
@ -50,18 +50,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
List<DifficultyHitObject> taikoDifficultyHitObjects = new List<DifficultyHitObject>();
List<DifficultyHitObject> difficultyHitObjects = new List<DifficultyHitObject>();
List<TaikoDifficultyHitObject> centreObjects = new List<TaikoDifficultyHitObject>();
List<TaikoDifficultyHitObject> rimObjects = new List<TaikoDifficultyHitObject>();
List<TaikoDifficultyHitObject> noteObjects = new List<TaikoDifficultyHitObject>();
for (int i = 2; i < beatmap.HitObjects.Count; i++)
{
taikoDifficultyHitObjects.Add(
difficultyHitObjects.Add(
new TaikoDifficultyHitObject(
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, taikoDifficultyHitObjects, taikoDifficultyHitObjects.Count
)
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects,
centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count)
);
}
return taikoDifficultyHitObjects;
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
return difficultyHitObjects;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@ -69,28 +74,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
var colour = (Colour)skills[0];
var rhythm = (Rhythm)skills[1];
var stamina = (Stamina)skills[2];
var combined = (Peaks)skills[0];
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
double colourRating = combined.ColourDifficultyValue * difficulty_multiplier;
double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier;
double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier;
double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
staminaRating *= staminaPenalty;
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
double starRating = rescale(combinedRating * 1.4);
//TODO : This is a temporary fix for the stamina rating of converts, due to their low colour variance.
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0 && colourRating < 0.05)
// TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
{
staminaPenalty *= 0.25;
starRating *= 0.925;
// For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
if (colourRating < 2 && staminaRating > 8)
starRating *= 0.80;
}
double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina, staminaPenalty);
double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
starRating = rescale(starRating);
HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
@ -101,75 +102,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
StaminaDifficulty = staminaRating,
RhythmDifficulty = rhythmRating,
ColourDifficulty = colourRating,
PeakDifficulty = combinedRating,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
};
}
/// <summary>
/// Calculates the penalty for the stamina skill for maps with low colour difficulty.
/// </summary>
/// <remarks>
/// Some maps (especially converts) can be easy to read despite a high note density.
/// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill.
/// </remarks>
private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty)
{
if (colorDifficulty <= 0) return 0.79 - 0.25;
return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2;
}
/// <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);
/// <summary>
/// Returns the partial 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 locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina stamina, double staminaPenalty)
{
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 * staminaPenalty;
double peak = norm(2, colourPeak, rhythmPeak, staminaPeak);
// 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.OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
/// <summary>
/// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars.
/// Applies a final re-scaling of the star rating.
/// </summary>
/// <param name="sr">The raw star rating value before re-scaling.</param>
private double rescale(double sr)

View File

@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())

View File

@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countMeh;
private int countMiss;
private double effectiveMissCount;
public TaikoPerformanceCalculator()
: base(new TaikoRuleset())
{
@ -35,7 +37,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0)
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
double multiplier = 1.13;
if (score.Mods.Any(m => m is ModHidden))
multiplier *= 1.075;
@ -55,6 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
Difficulty = difficultyValue,
Accuracy = accuracyValue,
EffectiveMissCount = effectiveMissCount,
Total = totalValue
};
}
@ -66,18 +73,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
difficultyValue *= Math.Pow(0.986, countMiss);
difficultyValue *= Math.Pow(0.986, effectiveMissCount);
if (score.Mods.Any(m => m is ModEasy))
difficultyValue *= 0.980;
difficultyValue *= 0.985;
if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025;
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.05 * lengthBonus;
if (score.Mods.Any(m => m is ModHardRock))
difficultyValue *= 1.050;
return difficultyValue * Math.Pow(score.Accuracy, 1.5);
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.050 * lengthBonus;
return difficultyValue * Math.Pow(score.Accuracy, 2.0);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@ -85,18 +95,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0)
return 0;
double accuracyValue = Math.Pow(140.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 12.0) * 27;
double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;
// Slight HDFL Bonus for accuracy.
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden))
accuracyValue *= 1.10 * lengthBonus;
accuracyValue *= Math.Max(1.050, 1.075 * lengthBonus);
return accuracyValue;
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
}

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
return;
base.OnMouseUp(e);
EndPlacement(true);
EndPlacement(spanPlacementObject.Duration > 0);
}
public override void UpdateTimeAndPosition(SnapResult result)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -8,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModEasy : ModEasy
{
public override string Description => @"Beats move slower, and less accuracy required!";
public override LocalisableString Description => @"Beats move slower, and less accuracy required!";
/// <summary>
/// Multiplier factor added to the scrolling speed.

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset<TaikoHitObject>
{
public override string Description => @"Beats fade out before you hit them!";
public override LocalisableString Description => @"Beats fade out before you hit them!";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
/// <summary>

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModRandom : ModRandom, IApplicableToBeatmap
{
public override string Description => @"Shuffle around the colours!";
public override LocalisableString Description => @"Shuffle around the colours!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(TaikoModSwap)).ToArray();
public void ApplyToBeatmap(IBeatmap beatmap)

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModRelax : ModRelax
{
public override string Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's.";
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's.";
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Beatmaps;
@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override string Name => "Swap";
public override string Acronym => "SW";
public override string Description => @"Dons become kats, kats become dons";
public override LocalisableString Description => @"Dons become kats, kats become dons";
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray();

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// </summary>
private const int rolling_hits_for_engaged_colour = 5;
public override Quad ScreenSpaceDrawQuad => MainPiece.Drawable.ScreenSpaceDrawQuad;
/// <summary>
/// Rolling number of tick hits. This increases for hits and decreases for misses.
/// </summary>

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
@ -20,6 +21,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
private Drawable backgroundLayer;
// required for editor blueprints (not sure why these circle pieces are zero size).
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
public LegacyCirclePiece()
{
RelativeSizeAxes = Axes.Both;

View File

@ -6,6 +6,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
@ -16,11 +17,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour
{
public override Quad ScreenSpaceDrawQuad
{
get
{
var headDrawQuad = headCircle.ScreenSpaceDrawQuad;
var tailDrawQuad = tailCircle.ScreenSpaceDrawQuad;
return new Quad(headDrawQuad.TopLeft, tailDrawQuad.TopRight, headDrawQuad.BottomLeft, tailDrawQuad.BottomRight);
}
}
private LegacyCirclePiece headCircle;
private Sprite body;
private Sprite end;
private Sprite tailCircle;
public LegacyDrumRoll()
{
@ -32,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
InternalChildren = new Drawable[]
{
end = new Sprite
tailCircle = new Sprite
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
@ -82,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
headCircle.AccentColour = colour;
body.Colour = colour;
end.Colour = colour;
tailCircle.Colour = colour;
}
}
}

View File

@ -25,6 +25,7 @@ using osu.Game.Scoring;
using System;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@ -192,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko
};
}
public override string GetDisplayNameForHitResult(HitResult result)
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{

View File

@ -84,12 +84,15 @@ namespace osu.Game.Tests.Gameplay
});
});
AddStep("reset clock", () => gameplayContainer.Start());
AddStep("reset clock", () => gameplayContainer.Reset(startClock: true));
AddUntilStep("sample played", () => sample.RequestedPlaying);
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
}
/// <summary>
/// Sample at 0ms, start time at 1000ms (so the sample should not be played).
/// </summary>
[Test]
public void TestSampleHasLifetimeEndWithInitialClockTime()
{
@ -104,12 +107,13 @@ namespace osu.Game.Tests.Gameplay
Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
{
StartTime = start_time,
Child = new FrameStabilityContainer
{
Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
}
});
gameplayContainer.Reset(start_time);
});
AddStep("start time", () => gameplayContainer.Start());
@ -143,7 +147,7 @@ namespace osu.Game.Tests.Gameplay
});
});
AddStep("start", () => gameplayContainer.Start());
AddStep("reset clock", () => gameplayContainer.Reset(startClock: true));
AddUntilStep("sample played", () => sample.IsPlayed);
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);

Some files were not shown because too many files have changed in this diff Show More