mirror of
https://github.com/ppy/osu.git
synced 2026-05-28 12:30:42 +08:00
Compare commits
687 Commits
+1
-1
@@ -113,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
|
||||
dotnet_style_predefined_type_for_member_access = true:warning
|
||||
csharp_style_var_when_type_is_apparent = true:none
|
||||
csharp_style_var_for_built_in_types = true:none
|
||||
csharp_style_var_for_built_in_types = false:warning
|
||||
csharp_style_var_elsewhere = true:silent
|
||||
|
||||
#Style - modifiers
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
github: ppy
|
||||
custom: https://osu.ppy.sh/home/support
|
||||
|
||||
@@ -50,6 +50,48 @@ jobs:
|
||||
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
|
||||
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
|
||||
|
||||
build-only-android:
|
||||
name: Build only (Android)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install .NET 5.0.x
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: "5.0.x"
|
||||
|
||||
- name: Setup MSBuild
|
||||
uses: microsoft/setup-msbuild@v1
|
||||
|
||||
- name: Build
|
||||
run: msbuild osu.Android.slnf /restore /p:Configuration=Debug
|
||||
|
||||
build-only-ios:
|
||||
# While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
|
||||
# See https://github.com/ppy/osu-framework/issues/4677 for the details.
|
||||
# The job can be unblocked once those issues are resolved and game deployments can happen again.
|
||||
if: false
|
||||
name: Build only (iOS)
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install .NET 5.0.x
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: "5.0.x"
|
||||
|
||||
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
|
||||
# cannot accept .sln(f) files as arguments.
|
||||
# Build just the main game for now.
|
||||
- name: Build
|
||||
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
|
||||
|
||||
inspect-code:
|
||||
name: Code Quality
|
||||
runs-on: ubuntu-latest
|
||||
@@ -79,9 +121,14 @@ jobs:
|
||||
run: |
|
||||
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
|
||||
# FIXME: Suppress warnings from templates project
|
||||
dotnet codefilesanity | while read -r line; do
|
||||
echo "::warning::$line"
|
||||
done
|
||||
exit_code=0
|
||||
while read -r line; do
|
||||
if [[ ! -z "$line" ]]; then
|
||||
echo "::error::$line"
|
||||
exit_code=1
|
||||
fi
|
||||
done <<< $(dotnet codefilesanity)
|
||||
exit $exit_code
|
||||
|
||||
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
|
||||
# - name: .NET Format (Dry Run)
|
||||
|
||||
@@ -30,3 +30,5 @@ jobs:
|
||||
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
|
||||
path: "*.trx"
|
||||
reporter: dotnet-trx
|
||||
list-suites: 'failed'
|
||||
list-tests: 'failed'
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1004.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1026.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. -->
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace osu.Desktop
|
||||
{
|
||||
lock (importableFiles)
|
||||
{
|
||||
var firstExtension = Path.GetExtension(filePaths.First());
|
||||
string firstExtension = Path.GetExtension(filePaths.First());
|
||||
|
||||
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
|
||||
|
||||
@@ -177,7 +177,7 @@ namespace osu.Desktop
|
||||
{
|
||||
Logger.Log($"Handling batch import of {importableFiles.Count} files");
|
||||
|
||||
var paths = importableFiles.ToArray();
|
||||
string[] paths = importableFiles.ToArray();
|
||||
importableFiles.Clear();
|
||||
|
||||
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
|
||||
|
||||
@@ -22,17 +22,17 @@ namespace osu.Desktop
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
var cwd = Environment.CurrentDirectory;
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
|
||||
string gameName = base_game_name;
|
||||
bool tournamentClient = false;
|
||||
|
||||
foreach (var arg in args)
|
||||
foreach (string arg in args)
|
||||
{
|
||||
var split = arg.Split('=');
|
||||
string[] split = arg.Split('=');
|
||||
|
||||
var key = split[0];
|
||||
var val = split.Length > 1 ? split[1] : string.Empty;
|
||||
string key = split[0];
|
||||
string val = split.Length > 1 ? split[1] : string.Empty;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace osu.Desktop
|
||||
{
|
||||
var importer = new ArchiveImportIPCChannel(host);
|
||||
|
||||
foreach (var file in args)
|
||||
foreach (string file in args)
|
||||
{
|
||||
Console.WriteLine(@"Importing {0}", file);
|
||||
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
|
||||
|
||||
@@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
||||
|
||||
[TestCase(4.050601681491468d, "diffcalc-test")]
|
||||
[TestCase(4.0505463516206195d, "diffcalc-test")]
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(5.169743871843191d, "diffcalc-test")]
|
||||
[TestCase(5.1696411260785498d, "diffcalc-test")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new CatchModDoubleTime());
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
protected CatchSelectionBlueprintTestScene()
|
||||
{
|
||||
EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
|
||||
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
|
||||
EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } };
|
||||
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
|
||||
{
|
||||
BeatLength = 100
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
protected override void AddHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
|
||||
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
|
||||
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
base.AddHitObject(hitObject);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public class TestSceneCatchDistanceSnapGrid : OsuManualInputManagerTestScene
|
||||
{
|
||||
private readonly ManualClock manualClock = new ManualClock();
|
||||
|
||||
[Cached(typeof(Playfield))]
|
||||
private readonly CatchPlayfield playfield;
|
||||
|
||||
private ScrollingHitObjectContainer hitObjectContainer => playfield.HitObjectContainer;
|
||||
|
||||
private readonly CatchDistanceSnapGrid distanceGrid;
|
||||
|
||||
private readonly FruitOutline fruitOutline;
|
||||
|
||||
private readonly Fruit fruit = new Fruit();
|
||||
|
||||
public TestSceneCatchDistanceSnapGrid()
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 500,
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScrollingTestContainer(ScrollingDirection.Down)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = playfield = new CatchPlayfield(new BeatmapDifficulty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Clock = new FramedClock(manualClock)
|
||||
}
|
||||
},
|
||||
distanceGrid = new CatchDistanceSnapGrid(new double[] { 0, -1, 1 }),
|
||||
fruitOutline = new FruitOutline()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
distanceGrid.StartTime = 100;
|
||||
distanceGrid.StartX = 250;
|
||||
|
||||
Vector2 screenSpacePosition = InputManager.CurrentState.Mouse.Position;
|
||||
|
||||
var result = distanceGrid.GetSnappedPosition(screenSpacePosition);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
fruit.OriginalX = hitObjectContainer.ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
|
||||
if (result.Time != null)
|
||||
fruit.StartTime = result.Time.Value;
|
||||
}
|
||||
|
||||
fruitOutline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, fruit);
|
||||
fruitOutline.UpdateFrom(fruit);
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
manualClock.CurrentTime -= e.ScrollDelta.Y * 50;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
@@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override IBeatmap GetPlayableBeatmap()
|
||||
{
|
||||
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5;
|
||||
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10;
|
||||
var playable = base.GetPlayableBeatmap();
|
||||
playable.Difficulty.SliderTickRate = 5;
|
||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
||||
return playable;
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
AddAssert("correct outline count", () =>
|
||||
{
|
||||
var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
|
||||
int expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
|
||||
return this.ChildrenOfType<FruitOutline>().Count() == expected;
|
||||
});
|
||||
AddAssert("correct vertex piece count", () =>
|
||||
@@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
X = x,
|
||||
Path = sliderPath,
|
||||
};
|
||||
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
|
||||
EditorBeatmap.Difficulty.SliderMultiplier = velocity;
|
||||
EditorBeatmap.Add(hitObject);
|
||||
EditorBeatmap.Update(hitObject);
|
||||
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Test]
|
||||
public void TestCatcherCatchWidth()
|
||||
{
|
||||
var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
|
||||
float halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
|
||||
AddStep("catch fruit", () =>
|
||||
{
|
||||
attemptCatch(new Fruit { X = -halfWidth + 1 });
|
||||
@@ -237,7 +237,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
for (int i = 0; i < count; i++)
|
||||
attemptCatch(hitObject(), out _, out _);
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private void spawnJuiceStream(bool hit = false)
|
||||
{
|
||||
var xCoords = getXCoords(hit);
|
||||
float xCoords = getXCoords(hit);
|
||||
|
||||
var juice = new JuiceStream
|
||||
{
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
||||
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
|
||||
|
||||
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
|
||||
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
|
||||
@@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1;
|
||||
double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
|
||||
double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
|
||||
float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
|
||||
float distanceToHyper = (float)(timeToNext * Catcher.BASE_DASH_SPEED - distanceToNext);
|
||||
|
||||
if (distanceToHyper < 0)
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
return new CatchDifficultyAttributes
|
||||
{
|
||||
@@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||
|
||||
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
|
||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
: base(hitObject, lastObject, clockRate)
|
||||
{
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
|
||||
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
||||
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// The guide lines used in the osu!catch editor to compose patterns that can be caught with constant speed.
|
||||
/// Currently, only forward placement (an object is snapped based on the previous object, not the opposite) is supported.
|
||||
/// </summary>
|
||||
public class CatchDistanceSnapGrid : CompositeDrawable
|
||||
{
|
||||
public double StartTime { get; set; }
|
||||
|
||||
public float StartX { get; set; }
|
||||
|
||||
private const double max_vertical_line_length_in_time = CatchPlayfield.WIDTH / Catcher.BASE_WALK_SPEED;
|
||||
|
||||
private readonly double[] velocities;
|
||||
|
||||
private readonly List<Path> verticalPaths = new List<Path>();
|
||||
|
||||
private readonly List<Vector2[]> verticalLineVertices = new List<Vector2[]>();
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
private ScrollingHitObjectContainer hitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
public CatchDistanceSnapGrid(double[] velocities)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.BottomLeft;
|
||||
|
||||
this.velocities = velocities;
|
||||
|
||||
for (int i = 0; i < velocities.Length; i++)
|
||||
{
|
||||
verticalPaths.Add(new SmoothPath
|
||||
{
|
||||
PathRadius = 2,
|
||||
Alpha = 0.5f,
|
||||
});
|
||||
|
||||
verticalLineVertices.Add(new[] { Vector2.Zero, Vector2.Zero });
|
||||
}
|
||||
|
||||
AddRangeInternal(verticalPaths);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double currentTime = hitObjectContainer.Time.Current;
|
||||
|
||||
for (int i = 0; i < velocities.Length; i++)
|
||||
{
|
||||
double velocity = velocities[i];
|
||||
|
||||
// The line ends at the top of the playfield.
|
||||
double endTime = hitObjectContainer.TimeAtPosition(-hitObjectContainer.DrawHeight, currentTime);
|
||||
|
||||
// Non-vertical lines are cut at the sides of the playfield.
|
||||
// Vertical lines are cut at some reasonable length.
|
||||
if (velocity > 0)
|
||||
endTime = Math.Min(endTime, StartTime + (CatchPlayfield.WIDTH - StartX) / velocity);
|
||||
else if (velocity < 0)
|
||||
endTime = Math.Min(endTime, StartTime + StartX / -velocity);
|
||||
else
|
||||
endTime = Math.Min(endTime, StartTime + max_vertical_line_length_in_time);
|
||||
|
||||
Vector2[] lineVertices = verticalLineVertices[i];
|
||||
lineVertices[0] = calculatePosition(velocity, StartTime);
|
||||
lineVertices[1] = calculatePosition(velocity, endTime);
|
||||
|
||||
var verticalPath = verticalPaths[i];
|
||||
verticalPath.Vertices = verticalLineVertices[i];
|
||||
verticalPath.OriginPosition = verticalPath.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
||||
Vector2 calculatePosition(double velocity, double time)
|
||||
{
|
||||
// Don't draw inverted lines.
|
||||
time = Math.Max(time, StartTime);
|
||||
|
||||
float x = StartX + (float)((time - StartTime) * velocity);
|
||||
float y = hitObjectContainer.PositionAtTime(time, currentTime);
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public SnapResult GetSnappedPosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
double time = hitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
// If the cursor is below the distance snap grid, snap to the origin.
|
||||
// Not returning `null` to retain the continuous snapping behavior when the cursor is slightly below the origin.
|
||||
// This behavior is not currently visible in the editor because editor chooses the snap start time based on the mouse position.
|
||||
if (time <= StartTime)
|
||||
{
|
||||
float y = hitObjectContainer.PositionAtTime(StartTime);
|
||||
Vector2 originPosition = hitObjectContainer.ToScreenSpace(new Vector2(StartX, y));
|
||||
return new SnapResult(originPosition, StartTime);
|
||||
}
|
||||
|
||||
return enumerateSnappingCandidates(time)
|
||||
.OrderBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private IEnumerable<SnapResult> enumerateSnappingCandidates(double time)
|
||||
{
|
||||
float y = hitObjectContainer.PositionAtTime(time);
|
||||
|
||||
foreach (double velocity in velocities)
|
||||
{
|
||||
float x = (float)(StartX + (time - StartTime) * velocity);
|
||||
Vector2 screenSpacePosition = hitObjectContainer.ToScreenSpace(new Vector2(x, y + hitObjectContainer.DrawHeight));
|
||||
yield return new SnapResult(screenSpacePosition, time);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,23 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@@ -17,6 +26,14 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
|
||||
{
|
||||
private const float distance_snap_radius = 50;
|
||||
|
||||
private CatchDistanceSnapGrid distanceSnapGrid;
|
||||
|
||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
public CatchHitObjectComposer(CatchRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
@@ -30,6 +47,27 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
|
||||
});
|
||||
|
||||
LayerBelowRuleset.Add(distanceSnapGrid = new CatchDistanceSnapGrid(new[]
|
||||
{
|
||||
0.0,
|
||||
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
|
||||
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
|
||||
}));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
updateDistanceSnapGrid();
|
||||
}
|
||||
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
|
||||
@@ -42,14 +80,95 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new BananaShowerCompositionTool()
|
||||
};
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||
{
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||
});
|
||||
|
||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
||||
// TODO: implement position snap
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
{
|
||||
result = snapResult;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
|
||||
|
||||
[CanBeNull]
|
||||
private PalpableCatchHitObject getLastSnappableHitObject(double time)
|
||||
{
|
||||
var hitObject = EditorBeatmap.HitObjects.OfType<CatchHitObject>().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case Fruit fruit:
|
||||
return fruit;
|
||||
|
||||
case JuiceStream juiceStream:
|
||||
return juiceStream.NestedHitObjects.OfType<PalpableCatchHitObject>().LastOrDefault(h => !(h is TinyDroplet));
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private PalpableCatchHitObject getDistanceSnapGridSourceHitObject()
|
||||
{
|
||||
switch (BlueprintContainer.CurrentTool)
|
||||
{
|
||||
case SelectTool _:
|
||||
if (EditorBeatmap.SelectedHitObjects.Count == 0)
|
||||
return null;
|
||||
|
||||
double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime);
|
||||
return getLastSnappableHitObject(minTime);
|
||||
|
||||
case FruitCompositionTool _:
|
||||
case JuiceStreamCompositionTool _:
|
||||
if (!CursorInPlacementArea)
|
||||
return null;
|
||||
|
||||
if (EditorBeatmap.PlacementObject.Value is JuiceStream)
|
||||
{
|
||||
// Juice stream path is not subject to snapping.
|
||||
return null;
|
||||
}
|
||||
|
||||
double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position);
|
||||
return getLastSnappableHitObject(timeAtCursor);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDistanceSnapGrid()
|
||||
{
|
||||
if (distanceSnapToggle.Value != TernaryState.True)
|
||||
{
|
||||
distanceSnapGrid.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceHitObject = getDistanceSnapGridSourceHitObject();
|
||||
|
||||
if (sourceHitObject == null)
|
||||
{
|
||||
distanceSnapGrid.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
distanceSnapGrid.Show();
|
||||
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
|
||||
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
var hitObject = drawable.HitObject;
|
||||
|
||||
var offset = hitObject.TimePreempt * fade_out_offset_multiplier;
|
||||
var duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
|
||||
double offset = hitObject.TimePreempt * fade_out_offset_multiplier;
|
||||
double duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
|
||||
|
||||
using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset))
|
||||
drawable.FadeOut(duration);
|
||||
|
||||
@@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
|
||||
|
||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
|
||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
||||
|
||||
@@ -26,9 +26,6 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
if (Beatmap.HitObjects.Count == 0)
|
||||
return;
|
||||
|
||||
// todo: add support for HT DT
|
||||
const double dash_speed = Catcher.BASE_SPEED;
|
||||
const double movement_speed = dash_speed / 2;
|
||||
float lastPosition = CatchPlayfield.CENTER_X;
|
||||
double lastTime = 0;
|
||||
|
||||
@@ -47,8 +44,8 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
|
||||
double speedRequired = positionChange == 0 ? 0 : positionChange / timeAvailable;
|
||||
|
||||
bool dashRequired = speedRequired > movement_speed;
|
||||
bool impossibleJump = speedRequired > movement_speed * 2;
|
||||
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
|
||||
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
|
||||
|
||||
// todo: get correct catcher size, based on difficulty CS.
|
||||
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
|
||||
@@ -73,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
else if (dashRequired)
|
||||
{
|
||||
// we do a movement in two parts - the dash part then the normal part...
|
||||
double timeAtNormalSpeed = positionChange / movement_speed;
|
||||
double timeAtNormalSpeed = positionChange / Catcher.BASE_WALK_SPEED;
|
||||
double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable;
|
||||
double timeAtDashSpeed = timeWeNeedToSave / 2;
|
||||
|
||||
@@ -86,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
}
|
||||
else
|
||||
{
|
||||
double timeBefore = positionChange / movement_speed;
|
||||
double timeBefore = positionChange / Catcher.BASE_WALK_SPEED;
|
||||
|
||||
addFrame(h.StartTime - timeBefore, lastPosition);
|
||||
addFrame(h.StartTime, h.EffectiveX);
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
public override void CollectPendingInputs(List<IInput> inputs)
|
||||
{
|
||||
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
float position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
|
||||
|
||||
inputs.Add(new CatchReplayState
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.Catcher:
|
||||
var version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
decimal version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
|
||||
if (version < 2.3m)
|
||||
{
|
||||
|
||||
@@ -57,14 +57,19 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public bool CatchFruitOnPlate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
|
||||
/// The speed of the catcher when the catcher is dashing.
|
||||
/// </summary>
|
||||
public const double BASE_SPEED = 1.0;
|
||||
public const double BASE_DASH_SPEED = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// The current speed of the catcher.
|
||||
/// The speed of the catcher when the catcher is not dashing.
|
||||
/// </summary>
|
||||
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
|
||||
public const double BASE_WALK_SPEED = 0.5;
|
||||
|
||||
/// <summary>
|
||||
/// The current speed of the catcher with the hyper-dash modifier applied.
|
||||
/// </summary>
|
||||
public double Speed => (Dashing ? BASE_DASH_SPEED : BASE_WALK_SPEED) * hyperDashModifier;
|
||||
|
||||
/// <summary>
|
||||
/// The amount by which caught fruit should be scaled down to fit on the plate.
|
||||
@@ -226,11 +231,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (result.IsHit && hitObject.HyperDash)
|
||||
{
|
||||
var target = hitObject.HyperDashTarget;
|
||||
var timeDifference = target.StartTime - hitObject.StartTime;
|
||||
double timeDifference = target.StartTime - hitObject.StartTime;
|
||||
double positionDifference = target.EffectiveX - X;
|
||||
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
|
||||
double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
|
||||
|
||||
SetHyperDashState(Math.Abs(velocity), target.EffectiveX);
|
||||
SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX);
|
||||
}
|
||||
else
|
||||
SetHyperDashState();
|
||||
@@ -266,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
|
||||
public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
|
||||
{
|
||||
var wasHyperDashing = HyperDashing;
|
||||
bool wasHyperDashing = HyperDashing;
|
||||
|
||||
if (modifier <= 1 || X == targetPosition)
|
||||
{
|
||||
|
||||
@@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Down;
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
|
||||
}
|
||||
|
||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
|
||||
|
||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty);
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
|
||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
||||
{
|
||||
var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
var pos = column.ScreenSpacePositionAtTime(time);
|
||||
|
||||
return new SnapResult(pos, time, column);
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Edit;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
@@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override float GetBeatSnapDistanceAt(double referenceTime)
|
||||
public override float GetBeatSnapDistanceAt(HitObject referenceObject)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override float DurationToDistance(double referenceTime, double duration)
|
||||
public override float DurationToDistance(HitObject referenceObject, double duration)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override double DistanceToDuration(double referenceTime, float distance)
|
||||
public override double DistanceToDuration(HitObject referenceObject, float distance)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
|
||||
public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
|
||||
public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
public int CompareTo(ConvertValue other)
|
||||
{
|
||||
var result = StartTime.CompareTo(other.StartTime);
|
||||
int result = StartTime.CompareTo(other.StartTime);
|
||||
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
private IEnumerable<ColumnType> getResults(StageDefinition definition)
|
||||
{
|
||||
for (var i = 0; i < definition.Columns; i++)
|
||||
for (int i = 0; i < definition.Columns; i++)
|
||||
yield return definition.GetTypeOfColumn(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
},
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
}
|
||||
|
||||
AddStep("load player", () =>
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||
|
||||
var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
|
||||
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
double roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
|
||||
double roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
if (IsForCurrentRuleset)
|
||||
{
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
|
||||
double roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty;
|
||||
IBeatmapDifficultyInfo difficulty = original.Difficulty;
|
||||
|
||||
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
|
||||
Random = new FastRandom(seed);
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Debug.Assert(distanceData != null);
|
||||
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
|
||||
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
|
||||
|
||||
double beatLength;
|
||||
#pragma warning disable 618
|
||||
@@ -55,13 +55,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
#pragma warning restore 618
|
||||
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
|
||||
else
|
||||
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
|
||||
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
|
||||
|
||||
SpanCount = repeatsData?.SpanCount() ?? 1;
|
||||
StartTime = (int)Math.Round(hitObject.StartTime);
|
||||
|
||||
// This matches stable's calculation.
|
||||
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
|
||||
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
|
||||
|
||||
SegmentDuration = (EndTime - StartTime) / SpanCount;
|
||||
}
|
||||
@@ -493,7 +493,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
if (!(HitObject is IHasPathWithRepeats curveData))
|
||||
return null;
|
||||
|
||||
var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
||||
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
||||
|
||||
// avoid slicing the list & creating copies, if at all possible.
|
||||
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
|
||||
|
||||
@@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
var pattern = new Pattern();
|
||||
|
||||
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out var addToCentre);
|
||||
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out bool addToCentre);
|
||||
|
||||
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
|
||||
int nextColumn = GetRandomColumn(upperBound: columnLimit);
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
if (drainTime == 0)
|
||||
drainTime = 10000;
|
||||
|
||||
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
|
||||
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
|
||||
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
|
||||
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
|
||||
|
||||
|
||||
@@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
{
|
||||
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
|
||||
v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)"))
|
||||
scrollTime => new SettingDescription(
|
||||
rawValue: scrollTime,
|
||||
name: "Scroll Speed",
|
||||
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,14 +41,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
|
||||
HitWindows hitWindows = new ManiaHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||
Mods = mods,
|
||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||
Skills = skills
|
||||
|
||||
@@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
var endTime = maniaCurrent.EndTime;
|
||||
var column = maniaCurrent.BaseObject.Column;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
int column = maniaCurrent.BaseObject.Column;
|
||||
|
||||
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
|
||||
|
||||
@@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
case PlayfieldType.Dual:
|
||||
{
|
||||
var keys = getDualStageKeyCount(variant);
|
||||
int keys = getDualStageKeyCount(variant);
|
||||
return $"{keys}K + {keys}K";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
|
||||
if (i < j)
|
||||
{
|
||||
T key = keys[i];
|
||||
keys[i] = keys[j];
|
||||
keys[j] = key;
|
||||
(keys[i], keys[j]) = (keys[j], keys[i]);
|
||||
}
|
||||
|
||||
i++;
|
||||
@@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
|
||||
while (i <= n / 2)
|
||||
{
|
||||
var child = 2 * i;
|
||||
int child = 2 * i;
|
||||
|
||||
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
|
||||
{
|
||||
@@ -142,11 +140,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
private static void swap(T[] a, int i, int j)
|
||||
{
|
||||
if (i != j)
|
||||
{
|
||||
T t = a[i];
|
||||
a[i] = a[j];
|
||||
a[j] = t;
|
||||
}
|
||||
(a[i], a[j]) = (a[j], a[i]);
|
||||
}
|
||||
|
||||
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
|
||||
@@ -154,11 +148,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
|
||||
if (a != b)
|
||||
{
|
||||
if (comparer.Compare(keys[a], keys[b]) > 0)
|
||||
{
|
||||
T key = keys[a];
|
||||
keys[a] = keys[b];
|
||||
keys[b] = key;
|
||||
}
|
||||
(keys[a], keys[b]) = (keys[b], keys[a]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
|
||||
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = availableColumns - 1 - h.Column);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
Seed.Value ??= RNG.Next();
|
||||
var rng = new Random((int)Seed.Value);
|
||||
|
||||
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList();
|
||||
|
||||
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (Time.Current < HitObject.StartTime)
|
||||
return;
|
||||
|
||||
var startTime = holdStartTime?.Invoke();
|
||||
double? startTime = holdStartTime?.Invoke();
|
||||
|
||||
if (startTime == null || startTime > HitObject.StartTime)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
{
|
||||
var currentObject = Beatmap.HitObjects[i];
|
||||
var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
|
||||
var releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
|
||||
double releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
|
||||
|
||||
yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column };
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,1,0,100,1,0
|
||||
10000,-150,4,1,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
51,192,500,128,0,1500:1:0:0:0:
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
{
|
||||
var keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
|
||||
string keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
|
||||
return this.GetAnimation(keyImage, true, true) != null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,11 +90,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
// Mania doesn't care about global velocity
|
||||
p.Velocity = 1;
|
||||
p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
|
||||
p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier;
|
||||
|
||||
// For non-mania beatmap, speed changes should only happen through timing points
|
||||
if (!isForCurrentRuleset)
|
||||
p.DifficultyPoint = new DifficultyControlPoint();
|
||||
p.EffectPoint = new EffectControlPoint();
|
||||
}
|
||||
|
||||
BarLines.ForEach(Playfield.Add);
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.Difficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
|
||||
@@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
|
||||
public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
|
||||
|
||||
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
|
||||
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
|
||||
|
||||
public double DistanceToDuration(double referenceTime, float distance) => distance;
|
||||
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
|
||||
|
||||
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
|
||||
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
|
||||
|
||||
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
|
||||
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
new Spinner
|
||||
{
|
||||
Duration = 2000,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
Duration = 6000,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -117,10 +117,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
PassCondition = checkSomeHit
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestApproachCirclesOnly() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModHidden { OnlyFadeApproachCircles = { Value = true } },
|
||||
Autoplay = true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(206, 142)
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Position = new Vector2(306, 142)
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(156, 242),
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), })
|
||||
},
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 7000,
|
||||
EndTime = 9000
|
||||
}
|
||||
}
|
||||
},
|
||||
PassCondition = checkSomeHit
|
||||
});
|
||||
|
||||
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
|
||||
|
||||
private bool objectWithIncreasedVisibilityHasIndex(int index)
|
||||
=> Player.Mods.Value.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
|
||||
=> Player.GameplayState.Mods.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
|
||||
|
||||
private class TestOsuModHidden : OsuModHidden
|
||||
{
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public class TestSceneOsuModNoScope : OsuModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestVisibleDuringBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 0 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 1000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 5000,
|
||||
}
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(2000, 4000),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
AddUntilStep("wait for start of break", isBreak);
|
||||
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for end of break", () => !isBreak());
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibleDuringSpinner()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 0 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 1000,
|
||||
},
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 2000,
|
||||
Duration = 2000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(300, 192),
|
||||
StartTime = 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
AddUntilStep("wait for start of spinner", isSpinning);
|
||||
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for end of spinner", () => !isSpinning());
|
||||
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibleAfterComboBreak()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModNoScope
|
||||
{
|
||||
HiddenComboCount = { Value = 2 },
|
||||
},
|
||||
Autoplay = true,
|
||||
PassCondition = () => true,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(100, 192),
|
||||
StartTime = 1000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(150, 192),
|
||||
StartTime = 3000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = new Vector2(200, 192),
|
||||
StartTime = 5000,
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddAssert("cursor must start visible", () => cursorAlphaAlmostEquals(1));
|
||||
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
|
||||
AddAssert("cursor must dim after combo", () => !cursorAlphaAlmostEquals(1));
|
||||
AddStep("break combo", () => Player.ScoreProcessor.Combo.Set(0));
|
||||
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
|
||||
}
|
||||
|
||||
private bool isSpinning() => Player.ChildrenOfType<DrawableSpinner>().SingleOrDefault()?.Progress > 0;
|
||||
|
||||
private bool isBreak() => Player.IsBreakTime.Value;
|
||||
|
||||
private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
|
||||
|
||||
[TestCase(6.6634445062299665d, "diffcalc-test")]
|
||||
[TestCase(1.0414203870195022d, "zero-length-sliders")]
|
||||
[TestCase(6.5867229481955389d, "diffcalc-test")]
|
||||
[TestCase(1.0416315570967911d, "zero-length-sliders")]
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(8.3858089051603368d, "diffcalc-test")]
|
||||
[TestCase(1.2723279173428435d, "zero-length-sliders")]
|
||||
[TestCase(8.2730989071947896d, "diffcalc-test")]
|
||||
[TestCase(1.2726413186221039d, "zero-length-sliders")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new OsuModDoubleTime());
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddSliderStep("circle size", 0f, 10f, 0f, val =>
|
||||
{
|
||||
config.SetValue(OsuSetting.AutoCursorSize, true);
|
||||
gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
|
||||
Scheduler.AddOnce(() => loadContent(false));
|
||||
gameplayState.Beatmap.Difficulty.CircleSize = val;
|
||||
Scheduler.AddOnce(loadContent);
|
||||
});
|
||||
|
||||
AddStep("test cursor container", () => loadContent(false));
|
||||
@@ -75,10 +75,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public void TestSizing(int circleSize, float userScale)
|
||||
{
|
||||
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
|
||||
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
|
||||
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
|
||||
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
|
||||
|
||||
AddStep("load content", () => loadContent());
|
||||
AddStep("load content", loadContent);
|
||||
|
||||
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
|
||||
|
||||
@@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
|
||||
}
|
||||
|
||||
private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null)
|
||||
private void loadContent() => loadContent(false);
|
||||
|
||||
private void loadContent(bool automated, Func<SkinProvidingContainer> skinProvider = null)
|
||||
{
|
||||
SetContents(_ =>
|
||||
{
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private void scheduleHit() => AddStep("schedule action", () =>
|
||||
{
|
||||
var delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
|
||||
double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
|
||||
Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton)), delay);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
};
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
};
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
|
||||
@@ -400,15 +400,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = hitObjects,
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||
BeatmapInfo =
|
||||
{
|
||||
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
|
||||
SelectedMods.Value = new[] { new OsuModClassic() };
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
@@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
HeadCircle.HitWindows = new TestHitWindows();
|
||||
|
||||
@@ -13,6 +13,7 @@ using osuTK.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
|
||||
var cpi = new LegacyControlPointInfo();
|
||||
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
|
||||
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = circleSize,
|
||||
SliderTickRate = 3
|
||||
});
|
||||
|
||||
var drawable = CreateDrawableSlider(slider);
|
||||
|
||||
|
||||
@@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
@@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
|
||||
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
|
||||
{
|
||||
private const double spinner_start_time = 100;
|
||||
private const double spinner_duration = 6000;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; }
|
||||
|
||||
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
|
||||
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
|
||||
|
||||
addSeekStep(5000);
|
||||
addSeekStep(spinner_start_time + 5000);
|
||||
AddStep("retrieve disc rotation", () =>
|
||||
{
|
||||
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
|
||||
@@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
|
||||
|
||||
addSeekStep(2500);
|
||||
addSeekStep(spinner_start_time + 2500);
|
||||
AddAssert("disc rotation rewound",
|
||||
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
|
||||
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
|
||||
@@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
|
||||
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
|
||||
|
||||
addSeekStep(5000);
|
||||
addSeekStep(spinner_start_time + 5000);
|
||||
AddAssert("is disc rotation almost same",
|
||||
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
|
||||
AddAssert("is symbol rotation almost same",
|
||||
@@ -140,12 +143,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestSpinnerNormalBonusRewinding()
|
||||
{
|
||||
addSeekStep(1000);
|
||||
addSeekStep(spinner_start_time + 1000);
|
||||
|
||||
AddAssert("player score matching expected bonus score", () =>
|
||||
{
|
||||
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||
double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
|
||||
});
|
||||
|
||||
@@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
||||
}
|
||||
|
||||
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
||||
{
|
||||
Frames = scoreReplay
|
||||
.Frames
|
||||
.Cast<OsuReplayFrame>()
|
||||
.Select(replayFrame =>
|
||||
{
|
||||
var adjustedTime = replayFrame.Time * rate;
|
||||
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
|
||||
})
|
||||
.Cast<ReplayFrame>()
|
||||
.ToList()
|
||||
};
|
||||
|
||||
private void addSeekStep(double time)
|
||||
{
|
||||
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
|
||||
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
}
|
||||
|
||||
@@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new Spinner
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
EndTime = 6000,
|
||||
StartTime = spinner_start_time,
|
||||
Duration = spinner_duration
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
},
|
||||
});
|
||||
|
||||
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
@@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
HeadCircle.HitWindows = new TestHitWindows();
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
{
|
||||
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
|
||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
|
||||
@@ -12,7 +12,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
public double FlashlightRating { get; set; }
|
||||
public double ApproachRate { get; set; }
|
||||
public double OverallDifficulty { get; set; }
|
||||
public double DrainRate { get; set; }
|
||||
public int HitCircleCount { get; set; }
|
||||
public int SliderCount { get; set; }
|
||||
public int SpinnerCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
speedRating = 0.0;
|
||||
|
||||
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseFlashlightPerformance = 0.0;
|
||||
@@ -53,13 +56,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||
|
||||
double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
|
||||
int maxCombo = beatmap.HitObjects.Count;
|
||||
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
||||
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
return new OsuDifficultyAttributes
|
||||
@@ -71,8 +76,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
FlashlightRating = flashlightRating,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = maxCombo,
|
||||
HitCircleCount = hitCirclesCount,
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
Skills = skills
|
||||
};
|
||||
@@ -95,10 +102,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||
hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
|
||||
hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
private int countMeh;
|
||||
private int countMiss;
|
||||
|
||||
private int effectiveMissCount;
|
||||
|
||||
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
|
||||
: base(ruleset, attributes, score)
|
||||
{
|
||||
@@ -39,16 +41,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
effectiveMissCount = calculateEffectiveMissCount();
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
if (mods.Any(m => m is OsuModNoFail))
|
||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
|
||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
|
||||
|
||||
if (mods.Any(m => m is OsuModSpunOut))
|
||||
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
{
|
||||
effectiveMissCount += countOk + countMeh;
|
||||
multiplier *= 0.6;
|
||||
}
|
||||
|
||||
double aimValue = computeAimValue();
|
||||
double speedValue = computeSpeedValue();
|
||||
double accuracyValue = computeAccuracyValue();
|
||||
@@ -91,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
aimValue *= lengthBonus;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (countMiss > 0)
|
||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
|
||||
if (effectiveMissCount > 0)
|
||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
@@ -108,9 +117,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
if (mods.Any(h => h is OsuModHidden))
|
||||
if (mods.Any(m => m is OsuModBlinds))
|
||||
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
|
||||
else if (mods.Any(h => h is OsuModHidden))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||
}
|
||||
|
||||
aimValue *= approachRateBonus;
|
||||
|
||||
@@ -132,8 +145,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= lengthBonus;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (countMiss > 0)
|
||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
||||
if (effectiveMissCount > 0)
|
||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
@@ -147,11 +160,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
|
||||
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
if (mods.Any(m => m is OsuModBlinds))
|
||||
{
|
||||
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||
speedValue *= 1.12;
|
||||
}
|
||||
else if (mods.Any(m => m is OsuModHidden))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||
}
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
|
||||
@@ -160,6 +182,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeAccuracyValue()
|
||||
{
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
return 0.0;
|
||||
|
||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
||||
double betterAccuracyPercentage;
|
||||
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
|
||||
@@ -180,8 +205,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||
|
||||
if (mods.Any(m => m is OsuModHidden))
|
||||
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||
if (mods.Any(m => m is OsuModBlinds))
|
||||
accuracyValue *= 1.14;
|
||||
else if (mods.Any(m => m is OsuModHidden))
|
||||
accuracyValue *= 1.08;
|
||||
|
||||
if (mods.Any(m => m is OsuModFlashlight))
|
||||
accuracyValue *= 1.02;
|
||||
|
||||
@@ -205,8 +234,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
flashlightValue *= 1.3;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (countMiss > 0)
|
||||
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
||||
if (effectiveMissCount > 0)
|
||||
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
@@ -224,6 +253,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
private int calculateEffectiveMissCount()
|
||||
{
|
||||
// guess the number of misses + slider breaks from combo
|
||||
double comboBasedMissCount = 0.0;
|
||||
|
||||
if (Attributes.SliderCount > 0)
|
||||
{
|
||||
double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
|
||||
if (scoreMaxCombo < fullComboThreshold)
|
||||
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
|
||||
}
|
||||
|
||||
// we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations
|
||||
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
|
||||
|
||||
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
|
||||
}
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
private void setDistances()
|
||||
{
|
||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||
if (BaseObject is Spinner || lastObject is Spinner)
|
||||
return;
|
||||
|
||||
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
|
||||
|
||||
@@ -71,11 +75,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
|
||||
|
||||
// Don't need to jump to reach spinners
|
||||
if (!(BaseObject is Spinner))
|
||||
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||
|
||||
if (lastLastObject != null)
|
||||
if (lastLastObject != null && !(lastLastObject is Spinner))
|
||||
{
|
||||
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
|
||||
|
||||
@@ -121,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
// Skip the head circle
|
||||
var scoringTimes = slider.NestedHitObjects.Skip(1).Select(t => t.StartTime);
|
||||
foreach (var time in scoringTimes)
|
||||
foreach (double time in scoringTimes)
|
||||
computeVertex(time);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,17 +22,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
}
|
||||
|
||||
protected override double SkillMultiplier => 26.25;
|
||||
protected override double StrainDecayBase => 0.15;
|
||||
private double currentStrain = 1;
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
private double skillMultiplier => 26.25;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double strainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
var osuCurrent = (OsuDifficultyHitObject)current;
|
||||
|
||||
double result = 0;
|
||||
double aimStrain = 0;
|
||||
|
||||
if (Previous.Count > 0)
|
||||
{
|
||||
@@ -42,11 +44,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
const double scale = 90;
|
||||
|
||||
var angleBonus = Math.Sqrt(
|
||||
double angleBonus = Math.Sqrt(
|
||||
Math.Max(osuPrevious.JumpDistance - scale, 0)
|
||||
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
|
||||
* Math.Max(osuCurrent.JumpDistance - scale, 0));
|
||||
result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
|
||||
aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +56,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
|
||||
|
||||
return Math.Max(
|
||||
result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
|
||||
aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
|
||||
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
|
||||
);
|
||||
}
|
||||
|
||||
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += strainValueOf(current) * skillMultiplier;
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
}
|
||||
|
||||
protected override double SkillMultiplier => 0.15;
|
||||
protected override double StrainDecayBase => 0.15;
|
||||
private double skillMultiplier => 0.15;
|
||||
private double strainDecayBase => 0.15;
|
||||
protected override double DecayWeight => 1.0;
|
||||
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
|
||||
private double currentStrain = 1;
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
private double strainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@@ -62,5 +63,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
return Math.Pow(smallDistNerf * result, 2.0);
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += strainValueOf(current) * skillMultiplier;
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
public abstract class OsuStrainSkill : StrainDecaySkill
|
||||
public abstract class OsuStrainSkill : StrainSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||
|
||||
@@ -16,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
public class Speed : OsuStrainSkill
|
||||
{
|
||||
private const double single_spacing_threshold = 125;
|
||||
|
||||
private const double angle_bonus_begin = 5 * Math.PI / 6;
|
||||
private const double pi_over_4 = Math.PI / 4;
|
||||
private const double pi_over_2 = Math.PI / 2;
|
||||
|
||||
protected override double SkillMultiplier => 1400;
|
||||
protected override double StrainDecayBase => 0.3;
|
||||
protected override int ReducedSectionCount => 5;
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
|
||||
private const double rhythm_multiplier = 0.75;
|
||||
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
|
||||
private const double min_speed_bonus = 75; // ~200BPM
|
||||
private const double speed_balancing_factor = 40;
|
||||
|
||||
private double skillMultiplier => 1375;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain = 1;
|
||||
private double currentRhythm = 1;
|
||||
|
||||
protected override int ReducedSectionCount => 5;
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
protected override int HistoryLength => 32;
|
||||
|
||||
private readonly double greatWindow;
|
||||
|
||||
public Speed(Mod[] mods, double hitWindowGreat)
|
||||
@@ -37,52 +39,138 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
greatWindow = hitWindowGreat;
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
/// <summary>
|
||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
private double calculateRhythmBonus(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
var osuCurrent = (OsuDifficultyHitObject)current;
|
||||
var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
|
||||
int previousIslandSize = 0;
|
||||
|
||||
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
|
||||
double strainTime = osuCurrent.StrainTime;
|
||||
double rhythmComplexitySum = 0;
|
||||
int islandSize = 1;
|
||||
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
|
||||
|
||||
bool firstDeltaSwitch = false;
|
||||
|
||||
for (int i = Previous.Count - 2; i > 0; i--)
|
||||
{
|
||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
|
||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
|
||||
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
|
||||
|
||||
double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
|
||||
|
||||
if (currHistoricalDecay != 0)
|
||||
{
|
||||
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
|
||||
|
||||
double currDelta = currObj.StrainTime;
|
||||
double prevDelta = prevObj.StrainTime;
|
||||
double lastDelta = lastObj.StrainTime;
|
||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
||||
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
|
||||
|
||||
windowPenalty = Math.Min(1, windowPenalty);
|
||||
|
||||
double effectiveRatio = windowPenalty * currRatio;
|
||||
|
||||
if (firstDeltaSwitch)
|
||||
{
|
||||
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
||||
{
|
||||
if (islandSize < 7)
|
||||
islandSize++; // island is still progressing, count size.
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
||||
effectiveRatio *= 0.125;
|
||||
|
||||
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
||||
effectiveRatio *= 0.25;
|
||||
|
||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
||||
effectiveRatio *= 0.25;
|
||||
|
||||
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
||||
effectiveRatio *= 0.50;
|
||||
|
||||
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||
effectiveRatio *= 0.125;
|
||||
|
||||
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
|
||||
|
||||
startRatio = effectiveRatio;
|
||||
|
||||
previousIslandSize = islandSize; // log the last island size.
|
||||
|
||||
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
||||
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
||||
|
||||
islandSize = 1;
|
||||
}
|
||||
}
|
||||
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
|
||||
{
|
||||
// Begin counting island until we change speed again.
|
||||
firstDeltaSwitch = true;
|
||||
startRatio = effectiveRatio;
|
||||
islandSize = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
}
|
||||
|
||||
private double strainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
// derive strainTime for calculation
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
|
||||
|
||||
double strainTime = osuCurrObj.StrainTime;
|
||||
double greatWindowFull = greatWindow * 2;
|
||||
double speedWindowRatio = strainTime / greatWindowFull;
|
||||
|
||||
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
|
||||
if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime)
|
||||
strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio);
|
||||
if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
|
||||
strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
|
||||
|
||||
// Cap deltatime to the OD 300 hitwindow.
|
||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
|
||||
|
||||
// derive speedBonus for calculation
|
||||
double speedBonus = 1.0;
|
||||
|
||||
if (strainTime < min_speed_bonus)
|
||||
speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||
|
||||
double angleBonus = 1.0;
|
||||
double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
|
||||
|
||||
if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin)
|
||||
{
|
||||
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
|
||||
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
|
||||
}
|
||||
|
||||
if (osuCurrent.Angle.Value < pi_over_2)
|
||||
{
|
||||
angleBonus = 1.28;
|
||||
if (distance < 90 && osuCurrent.Angle.Value < pi_over_4)
|
||||
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
|
||||
else if (distance < 90)
|
||||
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4);
|
||||
}
|
||||
}
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
return (1 + (speedBonus - 1) * 0.75)
|
||||
* angleBonus
|
||||
* (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5))
|
||||
/ strainTime;
|
||||
protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += strainValueOf(current) * skillMultiplier;
|
||||
|
||||
currentRhythm = calculateRhythmBonus(current);
|
||||
|
||||
return currentStrain * currentRhythm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
var oldPosition = slider.Position;
|
||||
var oldStartTime = slider.StartTime;
|
||||
double oldStartTime = slider.StartTime;
|
||||
|
||||
if (ControlPoint == slider.Path.ControlPoints[0])
|
||||
{
|
||||
@@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
if (!slider.Path.HasValidLength)
|
||||
{
|
||||
for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
|
||||
slider.Position = oldPosition;
|
||||
|
||||
@@ -8,12 +8,14 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@@ -67,6 +69,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
@@ -75,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
case SliderPlacementState.Initial:
|
||||
BeginPlacement();
|
||||
|
||||
var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
|
||||
|
||||
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
break;
|
||||
|
||||
@@ -212,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
||||
bodyPiece.UpdateFrom(HitObject);
|
||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||
|
||||
@@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updatePath()
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
editorBeatmap?.Update(HitObject);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,14 +47,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||
continue;
|
||||
|
||||
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
double deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
if (deltaTime >= hitObject.TimeFadeIn + hitObject.TimePreempt)
|
||||
// The objects are not visible at the same time (without mods), hence skipping.
|
||||
continue;
|
||||
|
||||
var distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared;
|
||||
var diameter = (hitObject.Radius - overlap_leniency) * 2;
|
||||
var diameterSq = diameter * diameter;
|
||||
float distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared;
|
||||
double diameter = (hitObject.Radius - overlap_leniency) * 2;
|
||||
double diameterSq = diameter * diameter;
|
||||
|
||||
bool areOverlapping = distanceSq < diameterSq;
|
||||
|
||||
|
||||
@@ -88,21 +88,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||
continue;
|
||||
|
||||
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
double deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
|
||||
// Ignore objects that are far enough apart in time to not be considered the same pattern.
|
||||
if (deltaTime > pattern_lifetime)
|
||||
continue;
|
||||
|
||||
// Relying on FastInvSqrt is probably good enough here. We'll be taking the difference between distances later, hence square not being sufficient.
|
||||
var distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast;
|
||||
float distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast;
|
||||
|
||||
// Ignore stacks and half-stacks, as these are close enough to where they can't be confused for being time-distanced.
|
||||
if (distance < stack_leniency)
|
||||
continue;
|
||||
|
||||
var observedTimeDistance = new ObservedTimeDistance(nextHitObject.StartTime, deltaTime, distance);
|
||||
var expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance);
|
||||
double expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance);
|
||||
|
||||
if (expectedDistance == 0)
|
||||
{
|
||||
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
|
||||
private double getExpectedDistance(IEnumerable<ObservedTimeDistance> prevObservedTimeDistances, ObservedTimeDistance observedTimeDistance)
|
||||
{
|
||||
var observations = prevObservedTimeDistances.Count();
|
||||
int observations = prevObservedTimeDistances.Count();
|
||||
|
||||
int count = 0;
|
||||
double sum = 0;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
|
||||
double od = context.Beatmap.Difficulty.OverallDifficulty;
|
||||
|
||||
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
|
||||
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
||||
{
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
|
||||
: base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
|
||||
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
|
||||
{
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize);
|
||||
int gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize);
|
||||
if (gridSizeIndex >= 0)
|
||||
currentGridSizeIndex = gridSizeIndex;
|
||||
updateSpacing();
|
||||
|
||||
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
base.LoadComplete();
|
||||
|
||||
var firstObj = beatmap.HitObjects[0];
|
||||
var startDelay = firstObj.StartTime - firstObj.TimePreempt;
|
||||
double startDelay = firstObj.StartTime - firstObj.TimePreempt;
|
||||
|
||||
using (BeginAbsoluteSequence(startDelay + break_close_late))
|
||||
leaveBreak();
|
||||
|
||||
@@ -5,6 +5,8 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -17,6 +19,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModHidden : ModHidden, IHidesApproachCircles
|
||||
{
|
||||
[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 double ScoreMultiplier => 1.06;
|
||||
|
||||
@@ -44,15 +49,15 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
applyState(hitObject, true);
|
||||
applyHiddenState(hitObject, true);
|
||||
}
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
applyState(hitObject, false);
|
||||
applyHiddenState(hitObject, false);
|
||||
}
|
||||
|
||||
private void applyState(DrawableHitObject drawableObject, bool increaseVisibility)
|
||||
private void applyHiddenState(DrawableHitObject drawableObject, bool increaseVisibility)
|
||||
{
|
||||
if (!(drawableObject is DrawableOsuHitObject drawableOsuObject))
|
||||
return;
|
||||
@@ -61,6 +66,24 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
(double fadeStartTime, double fadeDuration) = getFadeOutParameters(drawableOsuObject);
|
||||
|
||||
// process approach circle hiding first (to allow for early return below).
|
||||
if (!increaseVisibility)
|
||||
{
|
||||
if (drawableObject is DrawableHitCircle circle)
|
||||
{
|
||||
using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt))
|
||||
circle.ApproachCircle.Hide();
|
||||
}
|
||||
else if (drawableObject is DrawableSpinner spinner)
|
||||
{
|
||||
spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
|
||||
hideSpinnerApproachCircle(spinner);
|
||||
}
|
||||
}
|
||||
|
||||
if (OnlyFadeApproachCircles.Value)
|
||||
return;
|
||||
|
||||
switch (drawableObject)
|
||||
{
|
||||
case DrawableSliderTail _:
|
||||
@@ -84,12 +107,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// only fade the circle piece (not the approach circle) for the increased visibility object.
|
||||
fadeTarget = circle.CirclePiece;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we don't want to see the approach circle
|
||||
using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt))
|
||||
circle.ApproachCircle.Hide();
|
||||
}
|
||||
|
||||
using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
|
||||
fadeTarget.FadeOut(fadeDuration);
|
||||
@@ -111,9 +128,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// hide elements we don't care about.
|
||||
// todo: hide background
|
||||
|
||||
spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
|
||||
hideSpinnerApproachCircle(spinner);
|
||||
|
||||
using (spinner.BeginAbsoluteSequence(fadeStartTime))
|
||||
spinner.FadeOut(fadeDuration);
|
||||
|
||||
@@ -141,11 +155,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
static (double fadeStartTime, double fadeDuration) getParameters(OsuHitObject hitObject)
|
||||
{
|
||||
var fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn;
|
||||
var fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier;
|
||||
double fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn;
|
||||
double fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier;
|
||||
|
||||
// new duration from completed fade in to end (before fading out)
|
||||
var longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime;
|
||||
double longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
@@ -153,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
return (fadeOutStartTime, longFadeDuration);
|
||||
|
||||
case SliderTick _:
|
||||
var tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000);
|
||||
double tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000);
|
||||
return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration);
|
||||
|
||||
case Spinner _:
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor, IApplicableToPlayer, IApplicableToBeatmap
|
||||
{
|
||||
/// <summary>
|
||||
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
|
||||
/// </summary>
|
||||
private const float min_alpha = 0.0002f;
|
||||
|
||||
private const float transition_duration = 100;
|
||||
|
||||
public override string Name => "No Scope";
|
||||
public override string Acronym => "NS";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
|
||||
public override string Description => "Where's the cursor?";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
private BindableNumber<int> currentCombo;
|
||||
private IBindable<bool> isBreakTime;
|
||||
private PeriodTracker spinnerPeriods;
|
||||
|
||||
private float comboBasedAlpha;
|
||||
|
||||
[SettingSource(
|
||||
"Hidden at combo",
|
||||
"The combo count at which the cursor becomes completely hidden",
|
||||
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
|
||||
)]
|
||||
public BindableInt HiddenComboCount { get; } = new BindableInt
|
||||
{
|
||||
Default = 10,
|
||||
Value = 10,
|
||||
MinValue = 0,
|
||||
MaxValue = 50,
|
||||
};
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
isBreakTime = player.IsBreakTime.GetBoundCopy();
|
||||
}
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
spinnerPeriods = new PeriodTracker(beatmap.HitObjects.OfType<Spinner>().Select(b => new Period(b.StartTime - transition_duration, b.EndTime)));
|
||||
}
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
if (HiddenComboCount.Value == 0) return;
|
||||
|
||||
currentCombo = scoreProcessor.Combo.GetBoundCopy();
|
||||
currentCombo.BindValueChanged(combo =>
|
||||
{
|
||||
comboBasedAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public virtual void Update(Playfield playfield)
|
||||
{
|
||||
bool shouldAlwaysShowCursor = isBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime);
|
||||
float targetAlpha = shouldAlwaysShowCursor ? 1 : comboBasedAlpha;
|
||||
playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1));
|
||||
}
|
||||
}
|
||||
|
||||
public class HiddenComboSlider : OsuSliderBar<int>
|
||||
{
|
||||
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// to allow jumps and prevent too sharp turns during streams.
|
||||
|
||||
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
|
||||
var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f));
|
||||
double randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f));
|
||||
|
||||
current.AngleRad = (float)randomAngleRad + previous.AngleRad;
|
||||
if (current.AngleRad < 0)
|
||||
@@ -171,11 +171,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
// Clamp slider position to the placement area
|
||||
// If the slider is larger than the playfield, force it to stay at the original position
|
||||
var newX = possibleMovementBounds.Width < 0
|
||||
float newX = possibleMovementBounds.Width < 0
|
||||
? objectInfo.PositionOriginal.X
|
||||
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
|
||||
|
||||
var newY = possibleMovementBounds.Height < 0
|
||||
float newY = possibleMovementBounds.Height < 0
|
||||
? objectInfo.PositionOriginal.Y
|
||||
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
|
||||
|
||||
@@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
|
||||
// Take the circle radius into account.
|
||||
var radius = (float)slider.Radius;
|
||||
float radius = (float)slider.Radius;
|
||||
|
||||
minX -= radius;
|
||||
minY -= radius;
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// because the spinner is under the gameplay clock, it is affected by rate adjustments on the track;
|
||||
// for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time.
|
||||
// for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here.
|
||||
var rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate;
|
||||
double rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate;
|
||||
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
.Select(beat =>
|
||||
{
|
||||
var newCircle = new HitCircle();
|
||||
newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty);
|
||||
newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.Difficulty);
|
||||
newCircle.StartTime = beat;
|
||||
return (OsuHitObject)newCircle;
|
||||
}).ToList();
|
||||
@@ -193,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private IEnumerable<double> generateBeats(IBeatmap beatmap)
|
||||
{
|
||||
var startTime = originalHitObjects.First().StartTime;
|
||||
var endTime = originalHitObjects.Last().GetEndTime();
|
||||
double startTime = originalHitObjects.First().StartTime;
|
||||
double endTime = originalHitObjects.Last().GetEndTime();
|
||||
|
||||
var beats = beatmap.ControlPointInfo.TimingPoints
|
||||
// Ignore timing points after endTime
|
||||
@@ -208,9 +208,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
.ToList();
|
||||
|
||||
// Remove beats that are too close to the next one (e.g. due to timing point changes)
|
||||
for (var i = beats.Count - 2; i >= 0; i--)
|
||||
for (int i = beats.Count - 2; i >= 0; i--)
|
||||
{
|
||||
var beat = beats[i];
|
||||
double beat = beats[i];
|
||||
|
||||
if (!definitelyBigger(beats[i + 1] - beat, beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2))
|
||||
beats.RemoveAt(i);
|
||||
@@ -250,13 +250,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// Other kinds of combo info are also added in the process
|
||||
var combos = hitObjects.GroupBy(x => x.ComboIndex).ToList();
|
||||
|
||||
for (var i = 0; i < combos.Count; i++)
|
||||
for (int i = 0; i < combos.Count; i++)
|
||||
{
|
||||
var group = combos[i].ToList();
|
||||
group.First().NewCombo = true;
|
||||
group.Last().LastInCombo = true;
|
||||
|
||||
for (var j = 0; j < group.Count; j++)
|
||||
for (int j = 0; j < group.Count; j++)
|
||||
{
|
||||
var x = group[j];
|
||||
x.ComboIndex = i;
|
||||
@@ -273,17 +273,17 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
const float two_pi = MathF.PI * 2;
|
||||
|
||||
var direction = two_pi * nextSingle();
|
||||
var maxComboIndex = hitObjects.Last().ComboIndex;
|
||||
float direction = two_pi * nextSingle();
|
||||
int maxComboIndex = hitObjects.Last().ComboIndex;
|
||||
|
||||
for (var i = 0; i < hitObjects.Count; i++)
|
||||
for (int i = 0; i < hitObjects.Count; i++)
|
||||
{
|
||||
var obj = hitObjects[i];
|
||||
var lastPos = i == 0
|
||||
? Vector2.Divide(OsuPlayfield.BASE_SIZE, 2)
|
||||
: hitObjects[i - 1].Position;
|
||||
|
||||
var distance = maxComboIndex == 0
|
||||
float distance = maxComboIndex == 0
|
||||
? (float)obj.Radius
|
||||
: mapRange(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance);
|
||||
if (obj.NewCombo) distance *= 1.5f;
|
||||
@@ -292,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
// Attempt to place the circle at a place that does not overlap with previous ones
|
||||
|
||||
var tryCount = 0;
|
||||
int tryCount = 0;
|
||||
|
||||
// for checking overlap
|
||||
var precedingObjects = hitObjects.SkipLast(hitObjects.Count - i).TakeLast(overlap_check_count).ToList();
|
||||
@@ -363,7 +363,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
var beats = new List<double>();
|
||||
int i = 0;
|
||||
var currentTime = timingPoint.Time;
|
||||
double currentTime = timingPoint.Time;
|
||||
|
||||
while (!definitelyBigger(currentTime, mapEndTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint)
|
||||
{
|
||||
@@ -377,7 +377,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private OsuHitObject getClosestHitObject(List<OsuHitObject> hitObjects, double time)
|
||||
{
|
||||
var precedingIndex = hitObjects.FindLastIndex(h => h.StartTime < time);
|
||||
int precedingIndex = hitObjects.FindLastIndex(h => h.StartTime < time);
|
||||
|
||||
if (precedingIndex == hitObjects.Count - 1) return hitObjects[precedingIndex];
|
||||
|
||||
@@ -457,7 +457,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
private void clampToPlayfield(OsuHitObject obj)
|
||||
{
|
||||
var position = obj.Position;
|
||||
var radius = (float)obj.Radius;
|
||||
float radius = (float)obj.Radius;
|
||||
|
||||
if (position.Y < radius)
|
||||
position.Y = radius;
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
|
||||
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
|
||||
|
||||
GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime);
|
||||
GetFadeTimes(start, end, (float)d / distance, out double fadeInTime, out double fadeOutTime);
|
||||
|
||||
FollowPoint fp;
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
|
||||
// The lifetime start will match the fade-in time of the first follow point.
|
||||
float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length;
|
||||
FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _);
|
||||
FollowPointConnection.GetFadeTimes(Start, End, fraction, out double fadeInTime, out _);
|
||||
|
||||
LifetimeStart = fadeInTime;
|
||||
LifetimeEnd = double.MaxValue; // This will be set by the connection.
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
{
|
||||
var newEntry = new FollowPointLifetimeEntry(hitObject);
|
||||
|
||||
var index = lifetimeEntries.AddInPlace(newEntry, Comparer<FollowPointLifetimeEntry>.Create((e1, e2) =>
|
||||
int index = lifetimeEntries.AddInPlace(newEntry, Comparer<FollowPointLifetimeEntry>.Create((e1, e2) =>
|
||||
{
|
||||
int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime);
|
||||
|
||||
|
||||
@@ -140,9 +140,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
|
||||
|
||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
|
||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
|
||||
@@ -152,8 +151,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
foreach (var e in
|
||||
SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
|
||||
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
|
||||
|
||||
foreach (var e in sliderEvents)
|
||||
{
|
||||
switch (e.Type)
|
||||
{
|
||||
@@ -175,7 +175,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StartTime = e.Time,
|
||||
Position = Position,
|
||||
StackHeight = StackHeight,
|
||||
SampleControlPoint = SampleControlPoint,
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModBarrelRoll(),
|
||||
new OsuModApproachDifferent(),
|
||||
new OsuModMuted(),
|
||||
new OsuModNoScope(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
: base(beatmap, mods)
|
||||
{
|
||||
defaultHitWindows = new OsuHitWindows();
|
||||
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
defaultHitWindows.SetDifficulty(Beatmap.Difficulty.OverallDifficulty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -70,8 +70,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
Slider slider = drawableSlider.HitObject;
|
||||
|
||||
var span = slider.SpanAt(completionProgress);
|
||||
var spanProgress = slider.ProgressAt(completionProgress);
|
||||
int span = slider.SpanAt(completionProgress);
|
||||
double spanProgress = slider.ProgressAt(completionProgress);
|
||||
|
||||
double start = 0;
|
||||
double end = SnakingIn.Value ? Math.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1;
|
||||
@@ -110,8 +110,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
|
||||
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
|
||||
|
||||
var lastSnakedStart = SnakedStart ?? 0;
|
||||
var lastSnakedEnd = SnakedEnd ?? 0;
|
||||
double lastSnakedStart = SnakedStart ?? 0;
|
||||
double lastSnakedEnd = SnakedEnd ?? 0;
|
||||
|
||||
SnakedStart = null;
|
||||
SnakedEnd = null;
|
||||
|
||||
@@ -62,9 +62,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2));
|
||||
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2));
|
||||
|
||||
var delta = thisAngle - lastAngle;
|
||||
float delta = thisAngle - lastAngle;
|
||||
|
||||
if (Tracking)
|
||||
AddRotation(delta);
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
if (hasNumber)
|
||||
{
|
||||
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
||||
decimal? legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
||||
|
||||
if (legacyVersion >= 2.0m)
|
||||
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
// careful: need to call this exactly once for all calculations in a frame
|
||||
// as the function has a random factor in it
|
||||
var metreHeight = getMetreHeight(DrawableSpinner.Progress);
|
||||
float metreHeight = getMetreHeight(DrawableSpinner.Progress);
|
||||
|
||||
// hack to make the metre blink up from below than down from above.
|
||||
// move down the container to be able to apply masking for the metre,
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
return;
|
||||
|
||||
// Todo: This should probably not be done like this.
|
||||
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
|
||||
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2;
|
||||
|
||||
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
|
||||
{
|
||||
@@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
|
||||
//
|
||||
// We also need to apply the anti-clockwise rotation.
|
||||
var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
|
||||
double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
|
||||
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
|
||||
|
||||
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
if (autoCursorScale.Value && state != null)
|
||||
{
|
||||
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
|
||||
scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
|
||||
scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
|
||||
}
|
||||
|
||||
cursorScale.Value = scale;
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
/// <returns>The new position of the hit object, relative to the previous one.</returns>
|
||||
public static Vector2 RotateAwayFromEdge(Vector2 prevObjectPos, Vector2 posRelativeToPrev, float rotationRatio = 0.5f)
|
||||
{
|
||||
var relativeRotationDistance = 0f;
|
||||
float relativeRotationDistance = 0f;
|
||||
|
||||
if (prevObjectPos.X < playfield_middle.X)
|
||||
{
|
||||
@@ -88,16 +88,16 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
/// <returns>The rotated vector.</returns>
|
||||
public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio)
|
||||
{
|
||||
var initialAngleRad = MathF.Atan2(initial.Y, initial.X);
|
||||
var destAngleRad = MathF.Atan2(destination.Y, destination.X);
|
||||
float initialAngleRad = MathF.Atan2(initial.Y, initial.X);
|
||||
float destAngleRad = MathF.Atan2(destination.Y, destination.X);
|
||||
|
||||
var diff = destAngleRad - initialAngleRad;
|
||||
float diff = destAngleRad - initialAngleRad;
|
||||
|
||||
while (diff < -MathF.PI) diff += 2 * MathF.PI;
|
||||
|
||||
while (diff > MathF.PI) diff -= 2 * MathF.PI;
|
||||
|
||||
var finalAngleRad = initialAngleRad + rotationRatio * diff;
|
||||
float finalAngleRad = initialAngleRad + rotationRatio * diff;
|
||||
|
||||
return new Vector2(
|
||||
initial.Length * MathF.Cos(finalAngleRad),
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||
|
||||
[TestCase(2.2867022617692685d, "diffcalc-test")]
|
||||
[TestCase(2.2867022617692685d, "diffcalc-test-strong")]
|
||||
[TestCase(2.2420075288523802d, "diffcalc-test")]
|
||||
[TestCase(2.2420075288523802d, "diffcalc-test-strong")]
|
||||
public void Test(double expected, string name)
|
||||
=> base.Test(expected, name);
|
||||
|
||||
[TestCase(3.1704781712282624d, "diffcalc-test")]
|
||||
[TestCase(3.1704781712282624d, "diffcalc-test-strong")]
|
||||
[TestCase(3.134084469440479d, "diffcalc-test")]
|
||||
[TestCase(3.134084469440479d, "diffcalc-test-strong")]
|
||||
public void TestClockRateAdjusted(double expected, string name)
|
||||
=> Test(expected, name, new TaikoModDoubleTime());
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user