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

Merge remote-tracking branch 'upstream/master' into tournament-tools

This commit is contained in:
Dean Herbert 2019-03-02 13:11:34 +09:00
commit 796f6c3092
614 changed files with 7332 additions and 3325 deletions

View File

@ -1,8 +0,0 @@
Add any details pertaining to developers above the break.
- [ ] Depends on #PR
- Closes #ISSUE
---
Add a sentence or two describing this change in plain english. This will be displayed on the [changelog](https://osu.ppy.sh/home/changelog). A single screenshot or short gif is also welcomed.

6
.gitignore vendored
View File

@ -11,8 +11,10 @@
*.userprefs *.userprefs
### Cake ### ### Cake ###
tools/* tools/**
!tools/cakebuild.csproj build/tools/**
fastlane/report.xml
# Build results # Build results
bin/[Dd]ebug/ bin/[Dd]ebug/

14
.vscode/launch.json vendored
View File

@ -68,6 +68,20 @@
} }
}, },
"console": "internalConsole" "console": "internalConsole"
},
{
"name": "Cake: Debug Script",
"type": "coreclr",
"request": "launch",
"program": "${workspaceRoot}/build/tools/Cake.CoreCLR/0.30.0/Cake.dll",
"args": [
"${workspaceRoot}/build/build.cake",
"--debug",
"--verbosity=diagnostic"
],
"cwd": "${workspaceRoot}/build",
"stopAtEntry": true,
"externalConsole": false
} }
] ]
} }

6
Gemfile Normal file
View File

@ -0,0 +1,6 @@
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

173
Gemfile.lock Normal file
View File

@ -0,0 +1,173 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.0)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
atomos (0.1.3)
babosa (1.0.2)
claide (1.0.2)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.4.1)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.1)
emoji_regex (1.0.1)
excon (0.62.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.5)
fastlane (2.117.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.45.0, < 1.0.0)
faraday (~> 0.9)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.9)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.21.2, < 0.24.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
mini_magick (~> 4.5.1)
multi_json
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.2.2, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 1.6.2, < 2.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.6.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-clean_testflight_testers (0.2.0)
fastlane-plugin-souyuz (0.8.1)
souyuz (>= 0.8.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
google-api-client (0.23.9)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
google-cloud-core (1.3.0)
google-cloud-env (~> 1.0)
google-cloud-env (1.0.5)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
google-api-client (~> 0.23)
google-cloud-core (~> 1.2)
googleauth (>= 0.6.2, < 0.10.0)
googleauth (0.6.7)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
json (2.2.0)
jwt (2.1.0)
memoist (0.16.0)
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mini_magick (4.5.1)
mini_portile2 (2.4.0)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
nokogiri (1.10.1)
mini_portile2 (~> 2.4.0)
os (1.0.0)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.2.2)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.5)
CFPropertyList
naturally
slack-notifier (2.3.2)
souyuz (0.8.1)
fastlane (>= 2.29.0)
highline (~> 1.7)
nokogiri (~> 1.7)
terminal-notifier (1.8.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.6.1)
tty-screen (0.6.5)
tty-spinner (0.9.0)
tty-cursor (~> 0.6.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.4.1)
word_wrap (1.0.0)
xcodeproj (1.8.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
fastlane
fastlane-plugin-clean_testflight_testers
fastlane-plugin-souyuz
fastlane-plugin-xamarin
BUNDLED WITH
2.0.1

View File

@ -41,27 +41,28 @@ Param(
[switch]$ShowDescription, [switch]$ShowDescription,
[Alias("WhatIf", "Noop")] [Alias("WhatIf", "Noop")]
[switch]$DryRun, [switch]$DryRun,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
[string[]]$ScriptArgs [string[]]$ScriptArgs
) )
Write-Host "Preparing to run build script..." Write-Host "Preparing to run build script..."
# Determine the script root for resolving other paths. # Determine the script root for resolving other paths.
if(!$PSScriptRoot){ if(!$PSScriptRoot) {
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
} }
# Resolve the paths for resources used for debugging. # Resolve the paths for resources used for debugging.
$TOOLS_DIR = Join-Path $PSScriptRoot "tools" $BUILD_DIR = Join-Path $PSScriptRoot "build"
$CAKE_CSPROJ = Join-Path $TOOLS_DIR "cakebuild.csproj" $TOOLS_DIR = Join-Path $BUILD_DIR "tools"
$CAKE_CSPROJ = Join-Path $BUILD_DIR "cakebuild.csproj"
# Install the required tools locally. # Install the required tools locally.
Write-Host "Restoring cake tools..." Write-Host "Restoring cake tools..."
Invoke-Expression "dotnet restore `"$CAKE_CSPROJ`" --packages `"$TOOLS_DIR`"" | Out-Null Invoke-Expression "dotnet restore `"$CAKE_CSPROJ`" --packages `"$TOOLS_DIR`"" | Out-Null
# Find the Cake executable # Find the Cake executable
$CAKE_EXECUTABLE = (Get-ChildItem -Path ./tools/cake.coreclr/ -Filter Cake.dll -Recurse).FullName $CAKE_EXECUTABLE = (Get-ChildItem -Path "$TOOLS_DIR/cake.coreclr/" -Filter Cake.dll -Recurse).FullName
# Build Cake arguments # Build Cake arguments
$cakeArguments = @("$Script"); $cakeArguments = @("$Script");
@ -75,5 +76,7 @@ $cakeArguments += $ScriptArgs
# Start Cake # Start Cake
Write-Host "Running build script..." Write-Host "Running build script..."
Push-Location -Path $BUILD_DIR
Invoke-Expression "dotnet `"$CAKE_EXECUTABLE`" $cakeArguments" Invoke-Expression "dotnet `"$CAKE_EXECUTABLE`" $cakeArguments"
Pop-Location
exit $LASTEXITCODE exit $LASTEXITCODE

View File

@ -6,12 +6,13 @@
echo "Preparing to run build script..." echo "Preparing to run build script..."
cd build
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
TOOLS_DIR=$SCRIPT_DIR/tools TOOLS_DIR=$SCRIPT_DIR/tools
CAKE_BINARY_PATH=$TOOLS_DIR/"cake.coreclr" CAKE_BINARY_PATH=$TOOLS_DIR/"cake.coreclr"
SCRIPT="build.cake" SCRIPT="build.cake"
CAKE_CSPROJ=$TOOLS_DIR/"cakebuild.csproj" CAKE_CSPROJ=$SCRIPT_DIR/"cakebuild.csproj"
# Parse arguments. # Parse arguments.
CAKE_ARGUMENTS=() CAKE_ARGUMENTS=()

View File

@ -1,6 +1,7 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.21" #addin "nuget:?package=CodeFileSanity&version=0.0.21"
#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2018.2.2" #addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2018.2.2"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1" #tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS // ARGUMENTS
@ -9,30 +10,24 @@
var target = Argument("target", "Build"); var target = Argument("target", "Build");
var configuration = Argument("configuration", "Release"); var configuration = Argument("configuration", "Release");
var osuSolution = new FilePath("./osu.sln"); var rootDirectory = new DirectoryPath("..");
var solution = rootDirectory.CombineWithFilePath("osu.sln");
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// TASKS // TASKS
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
Task("Restore")
.Does(() => {
DotNetCoreRestore(osuSolution.FullPath);
});
Task("Compile") Task("Compile")
.IsDependentOn("Restore")
.Does(() => { .Does(() => {
DotNetCoreBuild(osuSolution.FullPath, new DotNetCoreBuildSettings { DotNetCoreBuild(solution.FullPath, new DotNetCoreBuildSettings {
Configuration = configuration, Configuration = configuration,
NoRestore = true,
}); });
}); });
Task("Test") Task("Test")
.IsDependentOn("Compile") .IsDependentOn("Compile")
.Does(() => { .Does(() => {
var testAssemblies = GetFiles("**/*.Tests/bin/**/*.Tests.dll"); var testAssemblies = GetFiles(rootDirectory + "/**/*.Tests/bin/**/*.Tests.dll");
DotNetCoreVSTest(testAssemblies, new DotNetCoreVSTestSettings { DotNetCoreVSTest(testAssemblies, new DotNetCoreVSTestSettings {
Logger = AppVeyor.IsRunningOnAppVeyor ? "Appveyor" : $"trx", Logger = AppVeyor.IsRunningOnAppVeyor ? "Appveyor" : $"trx",
@ -46,9 +41,7 @@ Task("InspectCode")
.WithCriteria(IsRunningOnWindows()) .WithCriteria(IsRunningOnWindows())
.IsDependentOn("Compile") .IsDependentOn("Compile")
.Does(() => { .Does(() => {
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First(); InspectCode(solution, new InspectCodeSettings {
InspectCode(osuSolution, new InspectCodeSettings {
CachesHome = "inspectcode", CachesHome = "inspectcode",
OutputFile = "inspectcodereport.xml", OutputFile = "inspectcodereport.xml",
}); });
@ -59,7 +52,7 @@ Task("InspectCode")
Task("CodeFileSanity") Task("CodeFileSanity")
.Does(() => { .Does(() => {
ValidateCodeSanity(new ValidateCodeSanitySettings { ValidateCodeSanity(new ValidateCodeSanitySettings {
RootDirectory = ".", RootDirectory = rootDirectory.FullPath,
IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
}); });
}); });

2
fastlane/Appfile Normal file
View File

@ -0,0 +1,2 @@
app_identifier("sh.ppy.osulazer") # The bundle identifier of your app
apple_id("apple-dev@ppy.sh") # Your Apple email address

65
fastlane/Fastfile Normal file
View File

@ -0,0 +1,65 @@
update_fastlane
default_platform(:ios)
platform :ios do
lane :testflight_prune_dry do
clean_testflight_testers(days_of_inactivity:45, dry_run: true)
end
# Specify a custom number for what's "inactive"
lane :testflight_prune do
clean_testflight_testers(days_of_inactivity: 45) # 120 days, so about 4 months
end
lane :update_version do |options|
options[:plist_path] = '../osu.iOS/Info.plist'
app_version(options)
end
desc 'Deploy to testflight'
lane :beta do |options|
update_version(options)
provision(
type: 'appstore'
)
build(
build_configuration: 'Release',
build_platform: 'iPhone'
)
client = HTTPClient.new
changelog = client.get_content 'https://gist.githubusercontent.com/peppy/ab89c29dcc0dce95f39eb218e8fad197/raw'
changelog.gsub!('$BUILD_ID', options[:build])
pilot(
wait_processing_interval: 900,
changelog: changelog,
ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa'
)
end
desc 'Compile the project'
lane :build do
nuget_restore(
project_path: 'osu.iOS.sln'
)
souyuz(
platform: "ios",
build_target: "osu_iOS",
plist_path: "../osu.iOS/Info.plist"
)
end
desc 'Install provisioning profiles using match'
lane :provision do |options|
if Helper.is_ci?
options[:readonly] = true
end
match(options)
end
end

1
fastlane/Matchfile Normal file
View File

@ -0,0 +1 @@
git_url('https://github.com/peppy/apple-certificates')

7
fastlane/Pluginfile Normal file
View File

@ -0,0 +1,7 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-clean_testflight_testers'
gem 'fastlane-plugin-souyuz'
gem 'fastlane-plugin-xamarin'

54
fastlane/README.md Normal file
View File

@ -0,0 +1,54 @@
fastlane documentation
================
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew cask install fastlane`
# Available Actions
## iOS
### ios testflight_prune_dry
```
fastlane ios testflight_prune_dry
```
### ios testflight_prune
```
fastlane ios testflight_prune
```
### ios update_version
```
fastlane ios update_version
```
### ios beta
```
fastlane ios beta
```
Deploy to testflight
### ios build
```
fastlane ios build
```
Compile the project
### ios provision
```
fastlane ios provision
```
Install provisioning profiles using match
----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@ -83,8 +83,7 @@ namespace osu.Desktop
public override void SetHost(GameHost host) public override void SetHost(GameHost host)
{ {
base.SetHost(host); base.SetHost(host);
var desktopWindow = host.Window as DesktopGameWindow; if (host.Window is DesktopGameWindow desktopWindow)
if (desktopWindow != null)
{ {
desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.CursorState |= CursorState.Hidden;

View File

@ -60,7 +60,7 @@ namespace osu.Desktop.Overlays
{ {
new OsuSpriteText new OsuSpriteText
{ {
Font = @"Exo2.0-Bold", Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = game.Name Text = game.Name
}, },
new OsuSpriteText new OsuSpriteText
@ -74,9 +74,8 @@ namespace osu.Desktop.Overlays
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
TextSize = 12, Font = OsuFont.Numeric.With(size: 12),
Colour = colours.Yellow, Colour = colours.Yellow,
Font = @"Venera",
Text = @"Development Build" Text = @"Development Build"
}, },
new Sprite new Sprite

View File

@ -34,13 +34,16 @@ namespace osu.Game.Rulesets.Catch.Tests
case JuiceStream stream: case JuiceStream stream:
foreach (var nested in stream.NestedHitObjects) foreach (var nested in stream.NestedHitObjects)
yield return new ConvertValue((CatchHitObject)nested); yield return new ConvertValue((CatchHitObject)nested);
break; break;
case BananaShower shower: case BananaShower shower:
foreach (var nested in shower.NestedHitObjects) foreach (var nested in shower.NestedHitObjects)
yield return new ConvertValue((CatchHitObject)nested); yield return new ConvertValue((CatchHitObject)nested);
break; break;
default: default:
yield return new ConvertValue((CatchHitObject)hitObject); yield return new ConvertValue((CatchHitObject)hitObject);
break; break;
} }
} }

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
public class CatchDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.2038001515546597d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset();
}
}

View File

@ -8,7 +8,8 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture] [TestFixture]
public class TestCaseCatchPlayer : Game.Tests.Visual.TestCasePlayer public class TestCaseCatchPlayer : Game.Tests.Visual.TestCasePlayer
{ {
public TestCaseCatchPlayer() : base(new CatchRuleset()) public TestCaseCatchPlayer()
: base(new CatchRuleset())
{ {
} }
} }

View File

@ -56,6 +56,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
rng.Next(); // osu!stable retrieved a random banana rotation rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour rng.Next(); // osu!stable retrieved a random banana colour
} }
break; break;
case JuiceStream juiceStream: case JuiceStream juiceStream:
foreach (var nested in juiceStream.NestedHitObjects) foreach (var nested in juiceStream.NestedHitObjects)
@ -67,6 +68,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
rng.Next(); // osu!stable retrieved a random droplet rotation rng.Next(); // osu!stable retrieved a random droplet rotation
hitObject.X = MathHelper.Clamp(hitObject.X, 0, 1); hitObject.X = MathHelper.Clamp(hitObject.X, 0, 1);
} }
break; break;
} }
} }

View File

@ -19,8 +19,10 @@ namespace osu.Game.Rulesets.Catch
{ {
[Description("Move left")] [Description("Move left")]
MoveLeft, MoveLeft,
[Description("Move right")] [Description("Move right")]
MoveRight, MoveRight,
[Description("Engage dash")] [Description("Engage dash")]
Dash, Dash,
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty namespace osu.Game.Rulesets.Catch.Difficulty
{ {
@ -10,10 +9,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public double ApproachRate; public double ApproachRate;
public int MaxCombo; public int MaxCombo;
public CatchDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)
{
}
} }
} }

View File

@ -1,148 +1,92 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Catch.Difficulty.Skills;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public class CatchDifficultyCalculator : DifficultyCalculator public class CatchDifficultyCalculator : DifficultyCalculator
{ {
/// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
/// </summary>
private const double strain_step = 750;
/// <summary>
/// The weighting of each strain value decays to this number * it's previous value
/// </summary>
private const double decay_weight = 0.94;
private const double star_scaling_factor = 0.145; private const double star_scaling_factor = 0.145;
protected override int SectionLength => 750;
private readonly float halfCatchWidth;
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty);
halfCatchWidth = catcher.CatchWidth * 0.5f;
// We're only using 80% of the catcher's width to simulate imperfect gameplay.
halfCatchWidth *= 0.8f;
} }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
if (!beatmap.HitObjects.Any()) if (beatmap.HitObjects.Count == 0)
return new CatchDifficultyAttributes(mods, 0); return new CatchDifficultyAttributes { Mods = mods };
var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty); // this is the same as osu!, so there's potential to share the implementation... maybe
float halfCatchWidth = catcher.CatchWidth * 0.5f; double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
var difficultyHitObjects = new List<CatchDifficultyHitObject>(); return new CatchDifficultyAttributes
foreach (var hitObject in beatmap.HitObjects)
{ {
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
Mods = mods,
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet))
};
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
CatchHitObject lastObject = null;
foreach (var hitObject in beatmap.HitObjects.OfType<CatchHitObject>())
{
if (lastObject == null)
{
lastObject = hitObject;
continue;
}
switch (hitObject) switch (hitObject)
{ {
// We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations. // We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations.
case Fruit fruit: case Fruit fruit:
difficultyHitObjects.Add(new CatchDifficultyHitObject(fruit, halfCatchWidth)); yield return new CatchDifficultyHitObject(fruit, lastObject, clockRate, halfCatchWidth);
lastObject = hitObject;
break; break;
case JuiceStream _: case JuiceStream _:
difficultyHitObjects.AddRange(hitObject.NestedHitObjects.OfType<CatchHitObject>().Where(o => !(o is TinyDroplet)).Select(o => new CatchDifficultyHitObject(o, halfCatchWidth))); foreach (var nested in hitObject.NestedHitObjects.OfType<CatchHitObject>().Where(o => !(o is TinyDroplet)))
{
yield return new CatchDifficultyHitObject(nested, lastObject, clockRate, halfCatchWidth);
lastObject = nested;
}
break; break;
} }
} }
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!calculateStrainValues(difficultyHitObjects, timeRate))
return new CatchDifficultyAttributes(mods, 0);
// this is the same as osu!, so there's potential to share the implementation... maybe
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate;
double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, timeRate)) * star_scaling_factor;
return new CatchDifficultyAttributes(mods, starRating)
{
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
MaxCombo = difficultyHitObjects.Count
};
} }
private bool calculateStrainValues(List<CatchDifficultyHitObject> objects, double timeRate) protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{ {
CatchDifficultyHitObject lastObject = null; new Movement(),
};
if (!objects.Any()) return false;
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
foreach (var currentObject in objects)
{
if (lastObject != null)
currentObject.CalculateStrains(lastObject, timeRate);
lastObject = currentObject;
}
return true;
}
private double calculateDifficulty(List<CatchDifficultyHitObject> objects, double timeRate)
{
// The strain step needs to be adjusted for the algorithm to be considered equal with speed changing mods
double actualStrainStep = strain_step * timeRate;
// Find the highest strain value within each strain step
var highestStrains = new List<double>();
double intervalEndTime = actualStrainStep;
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
CatchDifficultyHitObject previousHitObject = null;
foreach (CatchDifficultyHitObject hitObject in objects)
{
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
{
highestStrains.Add(maximumStrain);
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
// until the beginning of the next interval.
if (previousHitObject == null)
{
maximumStrain = 0;
}
else
{
double decay = Math.Pow(CatchDifficultyHitObject.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
maximumStrain = previousHitObject.Strain * decay;
}
// Go to the next time interval
intervalEndTime += actualStrainStep;
}
// Obtain maximum strain
maximumStrain = Math.Max(hitObject.Strain, maximumStrain);
previousHitObject = hitObject;
}
// Build the weighted sum over the highest strains for each interval
double difficulty = 0;
double weight = 1;
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
foreach (double strain in highestStrains)
{
difficulty += weight * strain;
weight *= decay_weight;
}
return difficulty;
}
} }
} }

View File

@ -1,130 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osuTK;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyHitObject
{
internal static readonly double DECAY_BASE = 0.20;
private const float normalized_hitobject_radius = 41.0f;
private const float absolute_player_positioning_error = 16f;
private readonly float playerPositioningError;
internal CatchHitObject BaseHitObject;
/// <summary>
/// Measures jump difficulty. CtB doesn't have something like button pressing speed or accuracy
/// </summary>
internal double Strain = 1;
/// <summary>
/// This is required to keep track of lazy player movement (always moving only as far as necessary)
/// Without this quick repeat sliders / weirdly shaped streams might become ridiculously overrated
/// </summary>
internal float PlayerPositionOffset;
internal float LastMovement;
internal float NormalizedPosition;
internal float ActualNormalizedPosition => NormalizedPosition + PlayerPositionOffset;
internal CatchDifficultyHitObject(CatchHitObject baseHitObject, float catcherWidthHalf)
{
BaseHitObject = baseHitObject;
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_hitobject_radius / catcherWidthHalf;
playerPositioningError = absolute_player_positioning_error; // * scalingFactor;
NormalizedPosition = baseHitObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
}
private const double direction_change_bonus = 12.5;
internal void CalculateStrains(CatchDifficultyHitObject previousHitObject, double timeRate)
{
// Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make.
// See Taiko feedback thread.
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000);
// Update new position with lazy movement.
PlayerPositionOffset =
MathHelper.Clamp(
previousHitObject.ActualNormalizedPosition,
NormalizedPosition - (normalized_hitobject_radius - playerPositioningError),
NormalizedPosition + (normalized_hitobject_radius - playerPositioningError)) // Obtain new lazy position, but be stricter by allowing for an error of a certain degree of the player.
- NormalizedPosition; // Subtract HitObject position to obtain offset
LastMovement = DistanceTo(previousHitObject);
double addition = spacingWeight(LastMovement);
if (NormalizedPosition < previousHitObject.NormalizedPosition)
{
LastMovement = -LastMovement;
}
CatchHitObject previousHitCircle = previousHitObject.BaseHitObject;
double additionBonus = 0;
double sqrtTime = Math.Sqrt(Math.Max(timeElapsed, 25));
// Direction changes give an extra point!
if (Math.Abs(LastMovement) > 0.1)
{
if (Math.Abs(previousHitObject.LastMovement) > 0.1 && Math.Sign(LastMovement) != Math.Sign(previousHitObject.LastMovement))
{
double bonus = direction_change_bonus / sqrtTime;
// Weight bonus by how
double bonusFactor = Math.Min(playerPositioningError, Math.Abs(LastMovement)) / playerPositioningError;
// We want time to play a role twice here!
addition += bonus * bonusFactor;
// Bonus for tougher direction switches and "almost" hyperdashes at this point
if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
{
additionBonus += 0.3 * bonusFactor;
}
}
// Base bonus for every movement, giving some weight to streams.
addition += 7.5 * Math.Min(Math.Abs(LastMovement), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtTime;
}
// Bonus for "almost" hyperdashes at corner points
if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
{
if (!previousHitCircle.HyperDash)
{
additionBonus += 1.0;
}
else
{
// After a hyperdash we ARE in the correct position. Always!
PlayerPositionOffset = 0;
}
addition *= 1.0 + additionBonus * ((10 - previousHitCircle.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
}
addition *= 850.0 / Math.Max(timeElapsed, 25);
Strain = previousHitObject.Strain * decay + addition;
}
private static double spacingWeight(float distance)
{
return Math.Pow(distance, 1.3) / 500;
}
internal float DistanceTo(CatchDifficultyHitObject other)
{
return Math.Abs(ActualNormalizedPosition - other.ActualNormalizedPosition);
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
{
public class CatchDifficultyHitObject : DifficultyHitObject
{
private const float normalized_hitobject_radius = 41.0f;
public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject;
public new CatchHitObject LastObject => (CatchHitObject)base.LastObject;
public readonly float NormalizedPosition;
public readonly float LastNormalizedPosition;
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public readonly double StrainTime;
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: 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;
NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
// Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure
StrainTime = Math.Max(25, DeltaTime);
}
}
}

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osuTK;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
public class Movement : Skill
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
private const double direction_change_bonus = 12.5;
protected override double SkillMultiplier => 850;
protected override double StrainDecayBase => 0.2;
protected override double DecayWeight => 0.94;
private float? lastPlayerPosition;
private float lastDistanceMoved;
protected override double StrainValueOf(DifficultyHitObject current)
{
var catchCurrent = (CatchDifficultyHitObject)current;
if (lastPlayerPosition == null)
lastPlayerPosition = catchCurrent.LastNormalizedPosition;
float playerPosition = MathHelper.Clamp(
lastPlayerPosition.Value,
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
);
float distanceMoved = playerPosition - lastPlayerPosition.Value;
double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 500;
double sqrtStrain = Math.Sqrt(catchCurrent.StrainTime);
double bonus = 0;
// Direction changes give an extra point!
if (Math.Abs(distanceMoved) > 0.1)
{
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
{
double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(distanceMoved)) / absolute_player_positioning_error;
distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor;
// Bonus for tougher direction switches and "almost" hyperdashes at this point
if (catchCurrent.LastObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH)
bonus = 0.3 * bonusFactor;
}
// Base bonus for every movement, giving some weight to streams.
distanceAddition += 7.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
}
// Bonus for "almost" hyperdashes at corner points
if (catchCurrent.LastObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH)
{
if (!catchCurrent.LastObject.HyperDash)
bonus += 1.0;
else
{
// After a hyperdash we ARE in the correct position. Always!
playerPosition = catchCurrent.NormalizedPosition;
}
distanceAddition *= 1.0 + bonus * ((10 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10);
}
lastPlayerPosition = playerPosition;
lastDistanceMoved = distanceMoved;
return distanceAddition / catchCurrent.StrainTime;
}
}
}

View File

@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Catch.MathUtils
/// <returns>The random value.</returns> /// <returns>The random value.</returns>
public uint NextUInt() public uint NextUInt()
{ {
uint t = _x ^ _x << 11; uint t = _x ^ (_x << 11);
_x = _y; _x = _y;
_y = _z; _y = _z;
_z = _w; _z = _w;
return _w = _w ^ _w >> 19 ^ t ^ t >> 8; return _w = _w ^ (_w >> 19) ^ t ^ (t >> 8);
} }
/// <summary> /// <summary>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Catch.Mods
return default_flashlight_size; return default_flashlight_size;
} }
protected override void OnComboChange(int newCombo) protected override void OnComboChange(ValueChangedEvent<int> e)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(newCombo)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
public override Color4 AccentColour public override Color4 AccentColour
{ {
get { return base.AccentColour; } get => base.AccentColour;
set set
{ {
base.AccentColour = value; base.AccentColour = value;

View File

@ -23,9 +23,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces
} }
private Color4 accentColour; private Color4 accentColour;
public Color4 AccentColour public Color4 AccentColour
{ {
get { return accentColour; } get => accentColour;
set set
{ {
accentColour = value; accentColour = value;

View File

@ -0,0 +1,138 @@
osu file format v14
[General]
StackLeniency: 0.3
Mode: 2
[Difficulty]
CircleSize:4
OverallDifficulty:7
ApproachRate:8.3
SliderMultiplier:1.6
SliderTickRate:1
[TimingPoints]
500,500,4,2,1,50,1,0
34500,-50,4,2,1,50,0,0
[HitObjects]
// fruits spaced 1/1 beat apart
32,128,0,5,0,0:0:0:0:
96,128,500,1,0,0:0:0:0:
160,128,1000,1,0,0:0:0:0:
224,128,1500,1,0,0:0:0:0:
288,128,2000,1,0,0:0:0:0:
352,128,2500,1,0,0:0:0:0:
416,128,3000,1,0,0:0:0:0:
480,128,3500,1,0,0:0:0:0:
// fruits spaced 1/2 beat apart
32,160,4500,1,0,0:0:0:0:
64,160,4750,1,0,0:0:0:0:
96,160,5000,1,0,0:0:0:0:
128,160,5250,1,0,0:0:0:0:
160,160,5500,1,0,0:0:0:0:
192,160,5750,1,0,0:0:0:0:
224,160,6000,1,0,0:0:0:0:
256,160,6250,1,0,0:0:0:0:
288,160,6500,1,0,0:0:0:0:
// fruits spaced 1/4 beat apart
96,128,7500,1,0,0:0:0:0:
128,128,7625,1,0,0:0:0:0:
160,128,7750,1,0,0:0:0:0:
192,128,7875,1,0,0:0:0:0:
224,128,8000,1,0,0:0:0:0:
256,128,8125,1,0,0:0:0:0:
288,128,8250,1,0,0:0:0:0:
320,128,8375,1,0,0:0:0:0:
352,128,8500,1,0,0:0:0:0:
// fruit hyperdashes, spaced 1/2 beat apart
32,160,9500,1,0,0:0:0:0:
480,160,9750,1,0,0:0:0:0:
32,160,10000,1,0,0:0:0:0:
480,160,10250,1,0,0:0:0:0:
32,160,10500,1,0,0:0:0:0:
480,160,10750,1,0,0:0:0:0:
32,160,11000,1,0,0:0:0:0:
// fruit hyperdashes, spaced 1/4 beat apart
32,192,12000,1,0,0:0:0:0:
480,192,12125,1,0,0:0:0:0:
32,192,12250,1,0,0:0:0:0:
480,192,12375,1,0,0:0:0:0:
32,192,12500,1,0,0:0:0:0:
480,192,12625,1,0,0:0:0:0:
32,192,12750,1,0,0:0:0:0:
480,192,12875,1,0,0:0:0:0:
32,192,13000,1,0,0:0:0:0:
// stream + hyperdash + stream, spaced 1/4 beat apart
32,192,14000,1,0,0:0:0:0:
64,192,14125,1,0,0:0:0:0:
96,192,14250,1,0,0:0:0:0:
128,192,14375,1,0,0:0:0:0:
480,192,14500,1,0,0:0:0:0:
448,192,14625,1,0,0:0:0:0:
416,192,14750,1,0,0:0:0:0:
384,192,14875,1,0,0:0:0:0:
32,192,15000,1,0,0:0:0:0:
// basic sliders
32,192,16000,2,0,L|192:192,1,160
224,192,17000,2,0,L|384:192,1,160
416,192,17875,2,0,L|480:192,1,40
// slider hyperdashes, spaced 1/4 beat apart
32,192,19000,2,0,L|128:192,1,80
480,192,19375,2,0,L|384:192,1,80
352,192,19750,2,0,L|256:192,1,80
0,192,20125,2,0,L|128:192,1,120
// stream + slider hyperdashes, spaced 1/4 beat apart
32,192,21500,1,0,0:0:0:0:
64,192,21625,1,0,0:0:0:0:
96,192,21750,1,0,0:0:0:0:
512,192,21875,2,0,L|320:192,1,160
320,192,22500,1,0,0:0:0:0:
288,192,22625,1,0,0:0:0:0:
256,192,22750,1,0,0:0:0:0:
0,192,22875,2,0,L|64:192,1,40
// streams, spaced 1/4 beat apart
64,192,24000,1,0,0:0:0:0:
160,192,24125,1,0,0:0:0:0:
64,192,24250,1,0,0:0:0:0:
160,192,24375,1,0,0:0:0:0:
64,192,24500,1,0,0:0:0:0:
160,192,24625,1,0,0:0:0:0:
64,192,24750,1,0,0:0:0:0:
160,192,24875,1,0,0:0:0:0:
64,192,25000,1,0,0:0:0:0:
160,192,25125,1,0,0:0:0:0:
64,192,25250,1,0,0:0:0:0:
160,192,25375,1,0,0:0:0:0:
64,192,25500,1,0,0:0:0:0:
// stream + spinner combo, spaced 1/4 beat apart
256,192,26500,12,0,27000,0:0:0:0:
128,192,27250,5,0,0:0:0:0:
128,192,27375,1,0,0:0:0:0:
160,192,27500,1,0,0:0:0:0:
192,192,27625,1,0,0:0:0:0:
256,192,27750,12,0,28500,0:0:0:0:
192,192,28625,5,0,0:0:0:0:
224,192,28750,1,0,0:0:0:0:
256,192,28875,1,0,0:0:0:0:
256,192,29000,1,0,0:0:0:0:
256,192,29125,12,0,29500,0:0:0:0:
// long slow slider
0,192,30500,6,0,B|480:192|480:192|0:192,2,960
// long fast slider
0,192,37500,6,0,B|480:192|480:192|0:192,2,960
// long hyperdash slider
0,192,41500,2,0,P|544:192|544:192,5,480

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
public class CatcherArea : Container public class CatcherArea : Container
{ {
public const float CATCHER_SIZE = 100; public const float CATCHER_SIZE = 106.75f;
protected internal readonly Catcher MovableCatcher; protected internal readonly Catcher MovableCatcher;
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
public Container ExplodingFruitTarget public Container ExplodingFruitTarget
{ {
set { MovableCatcher.ExplodingFruitTarget = value; } set => MovableCatcher.ExplodingFruitTarget = value;
} }
public CatcherArea(BeatmapDifficulty difficulty = null) public CatcherArea(BeatmapDifficulty difficulty = null)
@ -158,7 +158,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected bool Dashing protected bool Dashing
{ {
get { return dashing; } get => dashing;
set set
{ {
if (value == dashing) return; if (value == dashing) return;
@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
protected bool Trail protected bool Trail
{ {
get { return trail; } get => trail;
set set
{ {
if (value == trail) return; if (value == trail) return;

View File

@ -40,29 +40,29 @@ namespace osu.Game.Rulesets.Mania.Tests
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();
} }
public class ManiaConvertMapping : ConvertMapping<ConvertValue>, IEquatable<ManiaConvertMapping> public class ManiaConvertMapping : ConvertMapping<ConvertValue>, IEquatable<ManiaConvertMapping>
{ {
public uint RandomW; public uint RandomW;
public uint RandomX; public uint RandomX;
public uint RandomY; public uint RandomY;
public uint RandomZ; public uint RandomZ;
public ManiaConvertMapping() public ManiaConvertMapping()
{ {
} }
public ManiaConvertMapping(IBeatmapConverter converter) public ManiaConvertMapping(IBeatmapConverter converter)
{ {
var maniaConverter = (ManiaBeatmapConverter)converter; var maniaConverter = (ManiaBeatmapConverter)converter;
RandomW = maniaConverter.Random.W; RandomW = maniaConverter.Random.W;
RandomX = maniaConverter.Random.X; RandomX = maniaConverter.Random.X;
RandomY = maniaConverter.Random.Y; RandomY = maniaConverter.Random.Y;
RandomZ = maniaConverter.Random.Z; RandomZ = maniaConverter.Random.Z;
} }
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ; public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping); public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
} }
public struct ConvertValue : IEquatable<ConvertValue> public struct ConvertValue : IEquatable<ConvertValue>
{ {

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
public class ManiaDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3683365342338796d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}
}

View File

@ -3,7 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
TextSize = 14, Font = OsuFont.GetFont(size: 14),
Text = description Text = description
} }
} }

View File

@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
public int TargetColumns; public int TargetColumns;
public bool Dual;
public readonly bool IsForCurrentRuleset; public readonly bool IsForCurrentRuleset;
// Internal for testing purposes // Internal for testing purposes
@ -45,7 +46,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
if (IsForCurrentRuleset) if (IsForCurrentRuleset)
{
TargetColumns = (int)Math.Max(1, roundedCircleSize); TargetColumns = (int)Math.Max(1, roundedCircleSize);
if (TargetColumns >= 10)
{
TargetColumns = TargetColumns / 2;
Dual = true;
}
}
else else
{ {
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count; float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count;
@ -70,14 +78,22 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return base.ConvertBeatmap(original); return base.ConvertBeatmap(original);
} }
protected override Beatmap<ManiaHitObject> CreateBeatmap() => beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }); protected override Beatmap<ManiaHitObject> CreateBeatmap()
{
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
if (Dual)
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
return beatmap;
}
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap) protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
{ {
var maniaOriginal = original as ManiaHitObject; if (original is ManiaHitObject maniaOriginal)
if (maniaOriginal != null)
{ {
yield return maniaOriginal; yield return maniaOriginal;
yield break; yield break;
} }
@ -92,6 +108,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private readonly List<double> prevNoteTimes = new List<double>(max_notes_for_density); private readonly List<double> prevNoteTimes = new List<double>(max_notes_for_density);
private double density = int.MaxValue; private double density = int.MaxValue;
private void computeDensity(double newNoteTime) private void computeDensity(double newNoteTime)
{ {
if (prevNoteTimes.Count == max_notes_for_density) if (prevNoteTimes.Count == max_notes_for_density)
@ -104,6 +121,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private double lastTime; private double lastTime;
private Vector2 lastPosition; private Vector2 lastPosition;
private PatternType lastStair = PatternType.Stair; private PatternType lastStair = PatternType.Stair;
private void recordNote(double time, Vector2 position) private void recordNote(double time, Vector2 position)
{ {
lastTime = time; lastTime = time;

View File

@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (originalPattern.HitObjects.Count() == 1) if (originalPattern.HitObjects.Count() == 1)
{ {
yield return originalPattern; yield return originalPattern;
yield break; yield break;
} }
@ -135,6 +136,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0); return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03); return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
} }
@ -142,6 +144,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0); return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0); return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
} }
@ -149,11 +152,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0); return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0); return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
} }
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0); return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0); return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
} }

View File

@ -116,10 +116,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
} }
if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
// If we convert to 7K + 1, let's not overload the special key // If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0) && (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column // Make sure the last column was not the centre column
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
{ {
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
int column = RandomStart + TotalColumns - lastColumn - 1; int column = RandomStart + TotalColumns - lastColumn - 1;
@ -172,6 +172,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern = generateRandomPatternWithMirrored(0.12, 0.38, 0.12); return pattern = generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
if (ConversionDifficulty > 4) if (ConversionDifficulty > 4)
return pattern = generateRandomPatternWithMirrored(0.12, 0.17, 0); return pattern = generateRandomPatternWithMirrored(0.12, 0.17, 0);
return pattern = generateRandomPatternWithMirrored(0.12, 0, 0); return pattern = generateRandomPatternWithMirrored(0.12, 0, 0);
} }
@ -179,6 +180,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return pattern = generateRandomPattern(0.78, 0.42, 0, 0); return pattern = generateRandomPattern(0.78, 0.42, 0, 0);
return pattern = generateRandomPattern(1, 0.62, 0, 0); return pattern = generateRandomPattern(1, 0.62, 0, 0);
} }
@ -186,6 +188,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return pattern = generateRandomPattern(0.35, 0.08, 0, 0); return pattern = generateRandomPattern(0.35, 0.08, 0, 0);
return pattern = generateRandomPattern(0.52, 0.15, 0, 0); return pattern = generateRandomPattern(0.52, 0.15, 0, 0);
} }
@ -193,6 +196,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
if (convertType.HasFlag(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return pattern = generateRandomPattern(0.18, 0, 0, 0); return pattern = generateRandomPattern(0.18, 0, 0, 0);
return pattern = generateRandomPattern(0.45, 0, 0, 0); return pattern = generateRandomPattern(0.45, 0, 0, 0);
} }
@ -250,6 +254,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
} }
else else
last = GetRandomColumn(); last = GetRandomColumn();
return last; return last;
} }
} }

View File

@ -87,6 +87,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return 4; return 4;
if (val >= 1 - p3) if (val >= 1 - p3)
return 3; return 3;
return val >= 1 - p2 ? 2 : 1; return val >= 1 - p2 ? 2 : 1;
} }

View File

@ -12,51 +12,63 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
internal enum PatternType internal enum PatternType
{ {
None = 0, None = 0,
/// <summary> /// <summary>
/// Keep the same as last row. /// Keep the same as last row.
/// </summary> /// </summary>
ForceStack = 1 << 0, ForceStack = 1 << 0,
/// <summary> /// <summary>
/// Keep different from last row. /// Keep different from last row.
/// </summary> /// </summary>
ForceNotStack = 1 << 1, ForceNotStack = 1 << 1,
/// <summary> /// <summary>
/// Keep as single note at its original position. /// Keep as single note at its original position.
/// </summary> /// </summary>
KeepSingle = 1 << 2, KeepSingle = 1 << 2,
/// <summary> /// <summary>
/// Use a lower random value. /// Use a lower random value.
/// </summary> /// </summary>
LowProbability = 1 << 3, LowProbability = 1 << 3,
/// <summary> /// <summary>
/// Reserved. /// Reserved.
/// </summary> /// </summary>
Alternate = 1 << 4, Alternate = 1 << 4,
/// <summary> /// <summary>
/// Ignore the repeat count. /// Ignore the repeat count.
/// </summary> /// </summary>
ForceSigSlider = 1 << 5, ForceSigSlider = 1 << 5,
/// <summary> /// <summary>
/// Convert slider to circle. /// Convert slider to circle.
/// </summary> /// </summary>
ForceNotSlider = 1 << 6, ForceNotSlider = 1 << 6,
/// <summary> /// <summary>
/// Notes gathered together. /// Notes gathered together.
/// </summary> /// </summary>
Gathered = 1 << 7, Gathered = 1 << 7,
Mirror = 1 << 8, Mirror = 1 << 8,
/// <summary> /// <summary>
/// Change 0 -> 6. /// Change 0 -> 6.
/// </summary> /// </summary>
Reverse = 1 << 9, Reverse = 1 << 9,
/// <summary> /// <summary>
/// 1 -> 5 -> 1 -> 5 like reverse. /// 1 -> 5 -> 1 -> 5 like reverse.
/// </summary> /// </summary>
Cycle = 1 << 10, Cycle = 1 << 10,
/// <summary> /// <summary>
/// Next note will be at column + 1. /// Next note will be at column + 1.
/// </summary> /// </summary>
Stair = 1 << 11, Stair = 1 << 11,
/// <summary> /// <summary>
/// Next note will be at column - 1. /// Next note will be at column - 1.
/// </summary> /// </summary>

View File

@ -2,17 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty namespace osu.Game.Rulesets.Mania.Difficulty
{ {
public class ManiaDifficultyAttributes : DifficultyAttributes public class ManiaDifficultyAttributes : DifficultyAttributes
{ {
public double GreatHitWindow; public double GreatHitWindow;
public ManiaDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)
{
}
} }
} }

View File

@ -1,34 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty namespace osu.Game.Rulesets.Mania.Difficulty
{ {
internal class ManiaDifficultyCalculator : DifficultyCalculator public class ManiaDifficultyCalculator : DifficultyCalculator
{ {
private const double star_scaling_factor = 0.018; private const double star_scaling_factor = 0.018;
/// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size strain_step.
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
/// </summary>
private const double strain_step = 400;
/// <summary>
/// The weighting of each strain value decays to this number * it's previous value
/// </summary>
private const double decay_weight = 0.9;
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
@ -37,108 +27,70 @@ namespace osu.Game.Rulesets.Mania.Difficulty
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
} }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
if (!beatmap.HitObjects.Any()) if (beatmap.HitObjects.Count == 0)
return new ManiaDifficultyAttributes(mods, 0); return new ManiaDifficultyAttributes { Mods = mods };
var difficultyHitObjects = new List<ManiaHitObjectDifficulty>(); return new ManiaDifficultyAttributes
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
// Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change
difficultyHitObjects.AddRange(beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime));
if (!calculateStrainValues(difficultyHitObjects, timeRate))
return new ManiaDifficultyAttributes(mods, 0);
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor;
return new ManiaDifficultyAttributes(mods, starRating)
{ {
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future StarRating = difficultyValue(skills) * star_scaling_factor,
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate 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)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
}; };
} }
private bool calculateStrainValues(List<ManiaHitObjectDifficulty> objects, double timeRate) private double difficultyValue(Skill[] skills)
{ {
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. // Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
using (var hitObjectsEnumerator = objects.GetEnumerator()) var overall = skills.OfType<Overall>().Single();
var aggregatePeaks = new List<double>(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
foreach (var individual in skills.OfType<Individual>())
{ {
if (!hitObjectsEnumerator.MoveNext()) for (int i = 0; i < individual.StrainPeaks.Count; i++)
return false;
ManiaHitObjectDifficulty current = hitObjectsEnumerator.Current;
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
while (hitObjectsEnumerator.MoveNext())
{ {
var next = hitObjectsEnumerator.Current; double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
next?.CalculateStrains(current, timeRate);
current = next; if (aggregate > aggregatePeaks[i])
aggregatePeaks[i] = aggregate;
} }
return true;
}
}
private double calculateDifficulty(List<ManiaHitObjectDifficulty> objects, double timeRate)
{
double actualStrainStep = strain_step * timeRate;
// Find the highest strain value within each strain step
List<double> highestStrains = new List<double>();
double intervalEndTime = actualStrainStep;
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
ManiaHitObjectDifficulty previousHitObject = null;
foreach (var hitObject in objects)
{
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
{
highestStrains.Add(maximumStrain);
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
// until the beginning of the next interval.
if (previousHitObject == null)
{
maximumStrain = 0;
}
else
{
double individualDecay = Math.Pow(ManiaHitObjectDifficulty.INDIVIDUAL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
double overallDecay = Math.Pow(ManiaHitObjectDifficulty.OVERALL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
maximumStrain = previousHitObject.IndividualStrain * individualDecay + previousHitObject.OverallStrain * overallDecay;
}
// Go to the next time interval
intervalEndTime += actualStrainStep;
}
// Obtain maximum strain
double strain = hitObject.IndividualStrain + hitObject.OverallStrain;
maximumStrain = Math.Max(strain, maximumStrain);
previousHitObject = hitObject;
} }
// Build the weighted sum over the highest strains for each interval aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
double difficulty = 0; double difficulty = 0;
double weight = 1; double weight = 1;
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
foreach (double strain in highestStrains) // Difficulty is the weighted sum of the highest strains from every section.
foreach (double strain in aggregatePeaks)
{ {
difficulty += weight * strain; difficulty += strain * weight;
weight *= decay_weight; weight *= 0.9;
} }
return difficulty; return difficulty;
} }
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
for (int i = 1; i < beatmap.HitObjects.Count; i++)
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
}
protected override Skill[] CreateSkills(IBeatmap beatmap)
{
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
var skills = new List<Skill> { new Overall(columnCount) };
for (int i = 0; i < columnCount; i++)
skills.Add(new Individual(i, columnCount));
return skills.ToArray();
}
protected override Mod[] DifficultyAdjustmentMods protected override Mod[] DifficultyAdjustmentMods
{ {
get get

View File

@ -114,8 +114,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667) double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667)
* strainValue * strainValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer // Bonus for many hitcircles - it's harder to keep good accuracy up for longer
// accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); // accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
{
public class ManiaDifficultyHitObject : DifficultyHitObject
{
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
: base(hitObject, lastObject, clockRate)
{
}
}
}

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Individual : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.125;
private readonly double[] holdEndTimes;
private readonly int column;
public Individual(int column, int columnCount)
{
this.column = column;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime;
try
{
if (maniaCurrent.BaseObject.Column != column)
return 0;
// We give a slight bonus if something is held meanwhile
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
}
finally
{
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
}
}
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Overall : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.3;
private readonly double[] holdEndTimes;
private readonly int columnCount;
public Overall(int columnCount)
{
this.columnCount = columnCount;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = (maniaCurrent.BaseObject as HoldNote)?.EndTime ?? maniaCurrent.BaseObject.StartTime;
double holdFactor = 1.0; // Factor in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
for (int i = 0; i < columnCount; i++)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending.
// Releasing multiple notes at the same time is just as easy as releasing one
if (endTime == holdEndTimes[i])
holdAddition = 0;
// We give a slight bonus if something is held meanwhile
if (holdEndTimes[i] > endTime)
holdFactor = 1.25;
}
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
return (1 + holdAddition) * holdFactor;
}
}
}

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics; using osu.Game.Graphics;

View File

@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Mania
{ {
[Description("Special 1")] [Description("Special 1")]
Special1 = 1, Special1 = 1,
[Description("Special 2")] [Description("Special 2")]
Special2, Special2,
@ -26,38 +27,55 @@ namespace osu.Game.Rulesets.Mania
// above at a later time, without breaking replays/configs. // above at a later time, without breaking replays/configs.
[Description("Key 1")] [Description("Key 1")]
Key1 = 10, Key1 = 10,
[Description("Key 2")] [Description("Key 2")]
Key2, Key2,
[Description("Key 3")] [Description("Key 3")]
Key3, Key3,
[Description("Key 4")] [Description("Key 4")]
Key4, Key4,
[Description("Key 5")] [Description("Key 5")]
Key5, Key5,
[Description("Key 6")] [Description("Key 6")]
Key6, Key6,
[Description("Key 7")] [Description("Key 7")]
Key7, Key7,
[Description("Key 8")] [Description("Key 8")]
Key8, Key8,
[Description("Key 9")] [Description("Key 9")]
Key9, Key9,
[Description("Key 10")] [Description("Key 10")]
Key10, Key10,
[Description("Key 11")] [Description("Key 11")]
Key11, Key11,
[Description("Key 12")] [Description("Key 12")]
Key12, Key12,
[Description("Key 13")] [Description("Key 13")]
Key13, Key13,
[Description("Key 14")] [Description("Key 14")]
Key14, Key14,
[Description("Key 15")] [Description("Key 15")]
Key15, Key15,
[Description("Key 16")] [Description("Key 16")]
Key16, Key16,
[Description("Key 17")] [Description("Key 17")]
Key17, Key17,
[Description("Key 18")] [Description("Key 18")]
Key18, Key18,
} }

View File

@ -330,12 +330,12 @@ namespace osu.Game.Rulesets.Mania
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++) for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++)); bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
for (int i = 0; i < columns / 2; i++)
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
if (columns % 2 == 1) if (columns % 2 == 1)
bindings.Add(new KeyBinding(SpecialKey, SpecialAction)); bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
for (int i = 0; i < columns / 2; i++)
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
nextNormalAction = currentNormalAction; nextNormalAction = currentNormalAction;
return bindings; return bindings;
} }
@ -349,6 +349,7 @@ namespace osu.Game.Rulesets.Mania
/// Number of columns in this stage lies at (item - Single). /// Number of columns in this stage lies at (item - Single).
/// </summary> /// </summary>
Single = 0, Single = 0,
/// <summary> /// <summary>
/// Columns are grouped into two stages. /// Columns are grouped into two stages.
/// Overall number of columns lies at (item - Dual), further computation is required for /// Overall number of columns lies at (item - Dual), further computation is required for

View File

@ -37,11 +37,11 @@ namespace osu.Game.Rulesets.Mania.MathUtils
/// <returns>The random value.</returns> /// <returns>The random value.</returns>
public uint NextUInt() public uint NextUInt()
{ {
uint t = X ^ X << 11; uint t = X ^ (X << 11);
X = Y; X = Y;
Y = Z; Y = Z;
Z = W; Z = W;
return W = W ^ W >> 19 ^ t ^ t >> 8; return W = W ^ (W >> 19) ^ t ^ (t >> 8);
} }
/// <summary> /// <summary>

View File

@ -1,15 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToBeatmap<ManiaHitObject> public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter
{ {
public override string Name => "Dual Stages"; public override string Name => "Dual Stages";
public override string Acronym => "DS"; public override string Acronym => "DS";
@ -29,24 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods
if (isForCurrentRuleset) if (isForCurrentRuleset)
return; return;
mbc.TargetColumns *= 2; mbc.Dual = true;
}
public void ApplyToBeatmap(Beatmap<ManiaHitObject> beatmap)
{
if (isForCurrentRuleset)
return;
var maniaBeatmap = (ManiaBeatmap)beatmap;
var newDefinitions = new List<StageDefinition>();
foreach (var existing in maniaBeatmap.Stages)
{
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
}
maniaBeatmap.Stages = newDefinitions;
} }
public PlayfieldType PlayfieldType => PlayfieldType.Dual; public PlayfieldType PlayfieldType => PlayfieldType.Dual;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
@ -51,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Mods
} }
} }
protected override void OnComboChange(int newCombo) protected override void OnComboChange(ValueChangedEvent<int> e)
{ {
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@ -75,16 +76,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddNested(Tail); AddNested(Tail);
} }
protected override void OnDirectionChanged(ScrollingDirection direction) protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{ {
base.OnDirectionChanged(direction); base.OnDirectionChanged(e);
bodyPiece.Anchor = bodyPiece.Origin = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
} }
public override Color4 AccentColour public override Color4 AccentColour
{ {
get { return base.AccentColour; } get => base.AccentColour;
set set
{ {
base.AccentColour = value; base.AccentColour = value;

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public override Color4 AccentColour public override Color4 AccentColour
{ {
get { return base.AccentColour; } get => base.AccentColour;
set set
{ {
base.AccentColour = value; base.AccentColour = value;

View File

@ -3,7 +3,7 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override bool ShouldBeAlive => AlwaysAlive || base.ShouldBeAlive; protected override bool ShouldBeAlive => AlwaysAlive || base.ShouldBeAlive;
protected virtual void OnDirectionChanged(ScrollingDirection direction) protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{ {
Anchor = Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -31,16 +32,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
InternalChild = headPiece = new NotePiece(); InternalChild = headPiece = new NotePiece();
} }
protected override void OnDirectionChanged(ScrollingDirection direction) protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{ {
base.OnDirectionChanged(direction); base.OnDirectionChanged(e);
headPiece.Anchor = headPiece.Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; headPiece.Anchor = headPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
} }
public override Color4 AccentColour public override Color4 AccentColour
{ {
get { return base.AccentColour; } get => base.AccentColour;
set set
{ {
base.AccentColour = value; base.AccentColour = value;

View File

@ -77,11 +77,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public Color4 AccentColour public Color4 AccentColour
{ {
get { return accentColour; } get => accentColour;
set set
{ {
if (accentColour == value) if (accentColour == value)
return; return;
accentColour = value; accentColour = value;
updateAccentColour(); updateAccentColour();
@ -90,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public bool Hitting public bool Hitting
{ {
get { return hitting; } get => hitting;
set set
{ {
hitting = value; hitting = value;

View File

@ -35,13 +35,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
} }
private Color4 accentColour; private Color4 accentColour;
public Color4 AccentColour public Color4 AccentColour
{ {
get { return accentColour; } get => accentColour;
set set
{ {
if (accentColour == value) if (accentColour == value)
return; return;
accentColour = value; accentColour = value;
updateGlow(); updateGlow();

View File

@ -78,8 +78,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public Color4 AccentColour public Color4 AccentColour
{ {
get { return Colour; } get => Colour;
set { Colour = value; } set => Colour = value;
} }
} }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -49,20 +49,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
private void load(IScrollingInfo scrollingInfo) private void load(IScrollingInfo scrollingInfo)
{ {
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction => direction.BindValueChanged(dir =>
{ {
colouredBox.Anchor = colouredBox.Origin = direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; colouredBox.Anchor = colouredBox.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}, true); }, true);
} }
private Color4 accentColour; private Color4 accentColour;
public Color4 AccentColour public Color4 AccentColour
{ {
get { return accentColour; } get => accentColour;
set set
{ {
if (accentColour == value) if (accentColour == value)
return; return;
accentColour = value; accentColour = value;
colouredBox.Colour = AccentColour.Lighten(0.9f); colouredBox.Colour = AccentColour.Lighten(0.9f);

View File

@ -17,9 +17,10 @@ namespace osu.Game.Rulesets.Mania.Objects
public double EndTime => StartTime + Duration; public double EndTime => StartTime + Duration;
private double duration; private double duration;
public double Duration public double Duration
{ {
get { return duration; } get => duration;
set set
{ {
duration = value; duration = value;
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Objects
public override double StartTime public override double StartTime
{ {
get { return base.StartTime; } get => base.StartTime;
set set
{ {
base.StartTime = value; base.StartTime = value;
@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Objects
public override int Column public override int Column
{ {
get { return base.Column; } get => base.Column;
set set
{ {
base.Column = value; base.Column = value;

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types; using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Objects
public virtual int Column public virtual int Column
{ {
get => ColumnBindable; get => ColumnBindable.Value;
set => ColumnBindable.Value = value; set => ColumnBindable.Value = value;
} }

View File

@ -1,112 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Types;
using System;
namespace osu.Game.Rulesets.Mania.Objects
{
internal class ManiaHitObjectDifficulty
{
/// <summary>
/// Factor by how much individual / overall strain decays per second.
/// </summary>
/// <remarks>
/// These values are results of tweaking a lot and taking into account general feedback.
/// </remarks>
internal const double INDIVIDUAL_DECAY_BASE = 0.125;
internal const double OVERALL_DECAY_BASE = 0.30;
internal ManiaHitObject BaseHitObject;
private readonly int beatmapColumnCount;
private readonly double endTime;
private readonly double[] heldUntil;
/// <summary>
/// Measures jacks or more generally: repeated presses of the same button
/// </summary>
private readonly double[] individualStrains;
internal double IndividualStrain
{
get
{
return individualStrains[BaseHitObject.Column];
}
set
{
individualStrains[BaseHitObject.Column] = value;
}
}
/// <summary>
/// Measures note density in a way
/// </summary>
internal double OverallStrain = 1;
public ManiaHitObjectDifficulty(ManiaHitObject baseHitObject, int columnCount)
{
BaseHitObject = baseHitObject;
endTime = (baseHitObject as IHasEndTime)?.EndTime ?? baseHitObject.StartTime;
beatmapColumnCount = columnCount;
heldUntil = new double[beatmapColumnCount];
individualStrains = new double[beatmapColumnCount];
for (int i = 0; i < beatmapColumnCount; ++i)
{
individualStrains[i] = 0;
heldUntil[i] = 0;
}
}
internal void CalculateStrains(ManiaHitObjectDifficulty previousHitObject, double timeRate)
{
// TODO: Factor in holds
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
double individualDecay = Math.Pow(INDIVIDUAL_DECAY_BASE, timeElapsed / 1000);
double overallDecay = Math.Pow(OVERALL_DECAY_BASE, timeElapsed / 1000);
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
// Fill up the heldUntil array
for (int i = 0; i < beatmapColumnCount; ++i)
{
heldUntil[i] = previousHitObject.heldUntil[i];
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (BaseHitObject.StartTime < heldUntil[i] && endTime > heldUntil[i])
{
holdAddition = 1.0;
}
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (endTime == heldUntil[i])
{
holdAddition = 0;
}
// We give a slight bonus to everything if something is held meanwhile
if (heldUntil[i] > endTime)
{
holdFactor = 1.25;
}
// Decay individual strains
individualStrains[i] = previousHitObject.individualStrains[i] * individualDecay;
}
heldUntil[BaseHitObject.Column] = endTime;
// Increase individual strain in own column
IndividualStrain += 2.0 * holdFactor;
OverallStrain = previousHitObject.OverallStrain * overallDecay + (1.0 + holdAddition) * holdFactor;
}
}
}

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Objects
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)> private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
{ {
{ HitResult.Perfect, (44.8, 38.8, 27.8) }, { HitResult.Perfect, (44.8, 38.8, 27.8) },
{ HitResult.Great, (128, 98, 68 ) }, { HitResult.Great, (128, 98, 68) },
{ HitResult.Good, (194, 164, 134) }, { HitResult.Good, (194, 164, 134) },
{ HitResult.Ok, (254, 224, 194) }, { HitResult.Ok, (254, 224, 194) },
{ HitResult.Meh, (302, 272, 242) }, { HitResult.Meh, (302, 272, 242) },

View File

@ -0,0 +1,180 @@
osu file format v14
[General]
Mode: 3
[Difficulty]
CircleSize:4
OverallDifficulty:7
ApproachRate:8.3
SliderMultiplier:1.6
SliderTickRate:1
[TimingPoints]
500,500,4,2,1,50,1,0
37500,-50,4,2,1,50,0,0
41500,-25,4,2,1,50,0,0
[HitObjects]
// jacks spaced 1/1 beat apart
64,192,0,1,0,0:0:0:0:
64,192,500,1,0,0:0:0:0:
64,192,1000,1,0,0:0:0:0:
64,192,1500,1,0,0:0:0:0:
64,192,2000,1,0,0:0:0:0:
64,192,2500,1,0,0:0:0:0:
// jacks spaced 1/2 beat apart
64,192,3500,1,0,0:0:0:0:
64,192,3750,1,0,0:0:0:0:
64,192,4000,1,0,0:0:0:0:
64,192,4250,1,0,0:0:0:0:
64,192,4500,1,0,0:0:0:0:
64,192,4750,1,0,0:0:0:0:
64,192,5000,1,0,0:0:0:0:
64,192,6000,1,0,0:0:0:0:
// doubles jacks spaced 1/2 beat apart
192,192,6000,1,0,0:0:0:0:
64,192,6250,1,0,0:0:0:0:
192,192,6250,1,0,0:0:0:0:
64,192,6500,1,0,0:0:0:0:
192,192,6500,1,0,0:0:0:0:
64,192,6750,1,0,0:0:0:0:
192,192,6750,1,0,0:0:0:0:
64,192,7000,1,0,0:0:0:0:
192,192,7000,1,0,0:0:0:0:
64,192,7250,1,0,0:0:0:0:
192,192,7250,1,0,0:0:0:0:
64,192,7500,1,0,0:0:0:0:
192,192,7500,1,0,0:0:0:0:
// trill spaced 1/2 beat apart
64,192,8500,1,0,0:0:0:0:
192,192,8750,1,0,0:0:0:0:
64,192,9000,1,0,0:0:0:0:
192,192,9250,1,0,0:0:0:0:
64,192,9500,1,0,0:0:0:0:
192,192,9750,1,0,0:0:0:0:
64,192,10000,1,0,0:0:0:0:
192,192,10250,1,0,0:0:0:0:
64,192,10500,1,0,0:0:0:0:
// stair spaced 1/4 apart
64,192,11500,1,0,0:0:0:0:
192,192,11625,1,0,0:0:0:0:
320,192,11750,1,0,0:0:0:0:
448,192,11875,1,0,0:0:0:0:
320,192,12000,1,0,0:0:0:0:
192,192,12125,1,0,0:0:0:0:
64,192,12250,1,0,0:0:0:0:
192,192,12375,1,0,0:0:0:0:
320,192,12500,1,0,0:0:0:0:
448,192,12625,1,0,0:0:0:0:
// jumpstreams?
64,192,13500,1,0,0:0:0:0:
192,192,13625,1,0,0:0:0:0:
320,192,13750,1,0,0:0:0:0:
448,192,13875,1,0,0:0:0:0:
320,192,14000,1,0,0:0:0:0:
192,192,14000,1,0,0:0:0:0:
64,192,14125,1,0,0:0:0:0:
192,192,14250,1,0,0:0:0:0:
320,192,14250,1,0,0:0:0:0:
448,192,14250,1,0,0:0:0:0:
64,192,14375,1,0,0:0:0:0:
64,192,14500,1,0,0:0:0:0:
320,192,14625,1,0,0:0:0:0:
448,192,14625,1,0,0:0:0:0:
192,192,14625,1,0,0:0:0:0:
192,192,14750,1,0,0:0:0:0:
64,192,14875,1,0,0:0:0:0:
192,192,15000,1,0,0:0:0:0:
320,192,15125,1,0,0:0:0:0:
448,192,15125,1,0,0:0:0:0:
// double... jumps?
64,192,16000,1,0,0:0:0:0:
64,192,16250,1,0,0:0:0:0:
192,192,16250,1,0,0:0:0:0:
192,192,16500,1,0,0:0:0:0:
320,192,16500,1,0,0:0:0:0:
320,192,16750,1,0,0:0:0:0:
448,192,16750,1,0,0:0:0:0:
448,192,17000,1,0,0:0:0:0:
// notes alongside hold
64,192,18000,128,0,18500:0:0:0:0:
192,192,18000,1,0,0:0:0:0:
192,192,18250,1,0,0:0:0:0:
192,192,18500,1,0,0:0:0:0:
// notes overlapping hold
64,192,19500,1,0,0:0:0:0:
192,192,19625,128,0,20875:0:0:0:0:
64,192,19750,1,0,0:0:0:0:
64,192,20000,1,0,0:0:0:0:
64,192,20250,1,0,0:0:0:0:
64,192,20500,1,0,0:0:0:0:
64,192,20750,1,0,0:0:0:0:
64,192,21000,1,0,0:0:0:0:
// simultaneous holds
64,192,22000,128,0,23000:0:0:0:0:
192,192,22000,128,0,23000:0:0:0:0:
320,192,22000,128,0,23000:0:0:0:0:
448,192,22000,128,0,23000:0:0:0:0:
// hold stairs
64,192,24500,128,0,25500:0:0:0:0:
192,192,24625,128,0,25375:0:0:0:0:
320,192,24750,128,0,25250:0:0:0:0:
448,192,24875,128,0,25125:0:0:0:0:
448,192,25375,128,0,26375:0:0:0:0:
320,192,25500,128,0,26250:0:0:0:0:
192,192,25625,128,0,26125:0:0:0:0:
64,192,25750,128,0,26000:0:0:0:0:
// quads
64,192,26500,1,0,0:0:0:0:
64,192,27500,1,0,0:0:0:0:
192,192,27500,1,0,0:0:0:0:
320,192,27500,1,0,0:0:0:0:
448,192,27500,1,0,0:0:0:0:
64,192,27750,1,0,0:0:0:0:
192,192,27750,1,0,0:0:0:0:
320,192,27750,1,0,0:0:0:0:
448,192,27750,1,0,0:0:0:0:
64,192,28000,1,0,0:0:0:0:
192,192,28000,1,0,0:0:0:0:
320,192,28000,1,0,0:0:0:0:
448,192,28000,1,0,0:0:0:0:
64,192,28250,1,0,0:0:0:0:
192,192,28250,1,0,0:0:0:0:
320,192,28250,1,0,0:0:0:0:
448,192,28250,1,0,0:0:0:0:
64,192,28500,1,0,0:0:0:0:
192,192,28500,1,0,0:0:0:0:
320,192,28500,1,0,0:0:0:0:
448,192,28500,1,0,0:0:0:0:
// double-trills
64,192,29500,1,0,0:0:0:0:
192,192,29500,1,0,0:0:0:0:
320,192,29625,1,0,0:0:0:0:
448,192,29625,1,0,0:0:0:0:
64,192,29750,1,0,0:0:0:0:
192,192,29750,1,0,0:0:0:0:
320,192,29875,1,0,0:0:0:0:
448,192,29875,1,0,0:0:0:0:
64,192,30000,1,0,0:0:0:0:
192,192,30000,1,0,0:0:0:0:
320,192,30125,1,0,0:0:0:0:
448,192,30125,1,0,0:0:0:0:
64,192,30250,1,0,0:0:0:0:
192,192,30250,1,0,0:0:0:0:
320,192,30375,1,0,0:0:0:0:
448,192,30375,1,0,0:0:0:0:
64,192,30500,1,0,0:0:0:0:
192,192,30500,1,0,0:0:0:0:

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mania.UI.Components;
@ -82,28 +82,30 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer.Add(explosionContainer.CreateProxy()); TopLevelContainer.Add(explosionContainer.CreateProxy());
Direction.BindValueChanged(d => Direction.BindValueChanged(dir =>
{ {
hitTargetContainer.Padding = new MarginPadding hitTargetContainer.Padding = new MarginPadding
{ {
Top = d == ScrollingDirection.Up ? ManiaStage.HIT_TARGET_POSITION : 0, Top = dir.NewValue == ScrollingDirection.Up ? ManiaStage.HIT_TARGET_POSITION : 0,
Bottom = d == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0, Bottom = dir.NewValue == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0,
}; };
keyArea.Anchor = keyArea.Origin= d == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; keyArea.Anchor = keyArea.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
}, true); }, true);
} }
public override Axes RelativeSizeAxes => Axes.Y; public override Axes RelativeSizeAxes => Axes.Y;
private bool isSpecial; private bool isSpecial;
public bool IsSpecial public bool IsSpecial
{ {
get { return isSpecial; } get => isSpecial;
set set
{ {
if (isSpecial == value) if (isSpecial == value)
return; return;
isSpecial = value; isSpecial = value;
Width = isSpecial ? special_column_width : column_width; Width = isSpecial ? special_column_width : column_width;
@ -111,13 +113,15 @@ namespace osu.Game.Rulesets.Mania.UI
} }
private Color4 accentColour; private Color4 accentColour;
public Color4 AccentColour public Color4 AccentColour
{ {
get { return accentColour; } get => accentColour;
set set
{ {
if (accentColour == value) if (accentColour == value)
return; return;
accentColour = value; accentColour = value;
background.AccentColour = value; background.AccentColour = value;
@ -156,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.UI
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements) if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return; return;
explosionContainer.Add(new HitExplosion(judgedObject) explosionContainer.Add(new HitExplosion(judgedObject)
@ -167,7 +171,7 @@ namespace osu.Game.Rulesets.Mania.UI
public bool OnPressed(ManiaAction action) public bool OnPressed(ManiaAction action)
{ {
if (action != Action) if (action != Action.Value)
return false; return false;
var nextObject = var nextObject =

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
@ -48,9 +48,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}; };
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction => direction.BindValueChanged(dir =>
{ {
backgroundOverlay.Anchor = backgroundOverlay.Origin = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; backgroundOverlay.Anchor = backgroundOverlay.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
updateColours(); updateColours();
}, true); }, true);
} }
@ -70,6 +70,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{ {
if (accentColour == value) if (accentColour == value)
return; return;
accentColour = value; accentColour = value;
updateColours(); updateColours();

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -49,9 +49,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private void load(IScrollingInfo scrollingInfo) private void load(IScrollingInfo scrollingInfo)
{ {
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction => direction.BindValueChanged(dir =>
{ {
Anchor anchor = direction == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
hitTargetBar.Anchor = hitTargetBar.Origin = anchor; hitTargetBar.Anchor = hitTargetBar.Origin = anchor;
hitTargetLine.Anchor = hitTargetLine.Origin = anchor; hitTargetLine.Anchor = hitTargetLine.Origin = anchor;
@ -73,6 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{ {
if (accentColour == value) if (accentColour == value)
return; return;
accentColour = value; accentColour = value;
updateColours(); updateColours();

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
@ -64,11 +64,11 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}; };
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction => direction.BindValueChanged(dir =>
{ {
gradient.Colour = ColourInfo.GradientVertical( gradient.Colour = ColourInfo.GradientVertical(
direction == ScrollingDirection.Up ? Color4.Black : Color4.Black.Opacity(0), dir.NewValue == ScrollingDirection.Up ? Color4.Black : Color4.Black.Opacity(0),
direction == ScrollingDirection.Up ? Color4.Black.Opacity(0) : Color4.Black); dir.NewValue == ScrollingDirection.Up ? Color4.Black.Opacity(0) : Color4.Black);
}, true); }, true);
} }
@ -87,6 +87,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{ {
if (accentColour == value) if (accentColour == value)
return; return;
accentColour = value; accentColour = value;
updateColours(); updateColours();

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.UI
private void load() private void load()
{ {
if (JudgementText != null) if (JudgementText != null)
JudgementText.TextSize = 25; JudgementText.Font = JudgementText.Font.With(size: 25);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -4,7 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
@ -15,7 +15,6 @@ using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.UI
BarLines.ForEach(Playfield.Add); BarLines.ForEach(Playfield.Add);
Config.BindWith(ManiaSetting.ScrollDirection, configDirection); Config.BindWith(ManiaSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaSetting.ScrollTime, TimeRange); Config.BindWith(ManiaSetting.ScrollTime, TimeRange);
} }
@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
public override int Variant => (int)(Mods.OfType<IPlayfieldTypeMod>().FirstOrDefault()?.PlayfieldType ?? PlayfieldType.Single) + Beatmap.TotalColumns; public override int Variant => (int)(Beatmap.Stages.Count == 1 ? PlayfieldType.Single : PlayfieldType.Dual) + Beatmap.TotalColumns;
public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant); public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);

View File

@ -136,12 +136,12 @@ namespace osu.Game.Rulesets.Mania.UI
AddColumn(column); AddColumn(column);
} }
Direction.BindValueChanged(d => Direction.BindValueChanged(dir =>
{ {
barLineContainer.Padding = new MarginPadding barLineContainer.Padding = new MarginPadding
{ {
Top = d == ScrollingDirection.Up ? HIT_TARGET_POSITION : 0, Top = dir.NewValue == ScrollingDirection.Up ? HIT_TARGET_POSITION : 0,
Bottom = d == ScrollingDirection.Down ? HIT_TARGET_POSITION : 0, Bottom = dir.NewValue == ScrollingDirection.Down ? HIT_TARGET_POSITION : 0,
}; };
}, true); }, true);
} }
@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Mania.UI
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {
if (!judgedObject.DisplayResult || !DisplayJudgements) if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return; return;
judgements.Clear(); judgements.Clear();

View File

@ -33,9 +33,11 @@ namespace osu.Game.Rulesets.Osu.Tests
case Slider slider: case Slider slider:
foreach (var nested in slider.NestedHitObjects) foreach (var nested in slider.NestedHitObjects)
yield return createConvertValue(nested); yield return createConvertValue(nested);
break; break;
default: default:
yield return createConvertValue(hitObject); yield return createConvertValue(hitObject);
break; break;
} }

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class OsuDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.931145117263422, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new OsuRuleset();
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
private GameplayCursor cursor; private GameplayCursor cursor;
public override IReadOnlyList<Type> RequiredTypes => new [] { typeof(CursorTrail) }; public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(CursorTrail) };
public CursorContainer Cursor => cursor; public CursorContainer Cursor => cursor;

View File

@ -89,7 +89,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
private readonly bool auto; private readonly bool auto;
public TestDrawableHitCircle(HitCircle h, bool auto) : base(h) public TestDrawableHitCircle(HitCircle h, bool auto)
: base(h)
{ {
this.auto = auto; this.auto = auto;
} }

View File

@ -16,6 +16,7 @@ using osuTK.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -300,6 +301,7 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
private float judgementOffsetDirection = 1; private float judgementOffsetDirection = 1;
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {
var osuObject = judgedObject as DrawableOsuHitObject; var osuObject = judgedObject as DrawableOsuHitObject;
@ -313,7 +315,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = result.IsHit ? "Hit!" : "Miss!", Text = result.IsHit ? "Hit!" : "Miss!",
Colour = result.IsHit ? Color4.Green : Color4.Red, Colour = result.IsHit ? Color4.Green : Color4.Red,
TextSize = 30, Font = OsuFont.GetFont(size: 30),
Position = osuObject.HitObject.StackedEndPosition + judgementOffsetDirection * new Vector2(0, 45) Position = osuObject.HitObject.StackedEndPosition + judgementOffsetDirection * new Vector2(0, 45)
}); });

View File

@ -0,0 +1,372 @@
// 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 NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseSliderInput : RateAdjustedBeatmapTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SliderBall),
typeof(DrawableSlider),
typeof(DrawableSliderTick),
typeof(DrawableRepeatPoint),
typeof(DrawableOsuHitObject),
typeof(DrawableSliderHead),
typeof(DrawableSliderTail),
};
private const double time_before_slider = 250;
private const double time_slider_start = 1500;
private const double time_during_slide_1 = 2500;
private const double time_during_slide_2 = 3000;
private const double time_during_slide_3 = 3500;
private const double time_during_slide_4 = 3800;
private List<JudgementResult> judgementResults;
private bool allJudgedFired;
/// <summary>
/// Scenario:
/// - Press a key before a slider starts
/// - Press the other key on the slider head timed correctly while holding the original key
/// - Release the latter pressed key
/// Expected Result:
/// A passing test case will have the cursor lose tracking on replay frame 3.
/// </summary>
[Test]
public void TestInvalidKeyTransfer()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
});
AddAssert("Tracking lost", assertMidSliderJudgementFail);
}
/// <summary>
/// Scenario:
/// - Press a key on the slider head timed correctly
/// - Press the other key in the middle of the slider while holding the original key
/// - Release the original key used to hit the slider
/// Expected Result:
/// A passing test case will have the cursor continue tracking on replay frame 3.
/// </summary>
[Test]
public void TestLeftBeforeSliderThenRightThenLettingGoOfLeft()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_during_slide_1 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
});
AddAssert("Tracking retained", assertGreatJudge);
}
/// <summary>
/// Scenario:
/// - Press a key on the slider head timed correctly
/// - Press the other key in the middle of the slider while holding the original key
/// - Release the new key that was pressed second
/// Expected Result:
/// A passing test case will have the cursor continue tracking on replay frame 3.
/// </summary>
[Test]
public void TestTrackingRetentionLeftRightLeft()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
});
AddAssert("Tracking retained", assertGreatJudge);
}
/// <summary>
/// Scenario:
/// - Press a key before a slider starts
/// - Press the other key on the slider head timed correctly while holding the original key
/// - Release the key that was held down before the slider started.
/// Expected Result:
/// A passing test case will have the cursor continue tracking on replay frame 3
/// </summary>
[Test]
public void TestTrackingLeftBeforeSliderToRight()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
});
AddAssert("Tracking retained", assertGreatJudge);
}
/// <summary>
/// Scenario:
/// - Press a key before a slider starts
/// - Hold the key down throughout the slider without pressing any other buttons.
/// Expected Result:
/// A passing test case will have the cursor track the slider, but miss the slider head.
/// </summary>
[Test]
public void TestTrackingPreclicked()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
});
AddAssert("Tracking retained, sliderhead miss", assertHeadMissTailTracked);
}
/// <summary>
/// Scenario:
/// - Press a key before a slider starts
/// - Hold the key down after the slider starts
/// - Move the cursor away from the slider body
/// - Move the cursor back onto the body
/// Expected Result:
/// A passing test case will have the cursor track the slider, miss the head, miss the ticks where its outside of the body, and resume tracking when the cursor returns.
/// </summary>
[Test]
public void TestTrackingReturnMidSlider()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(150, 150), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
new OsuReplayFrame { Position = new Vector2(200, 200), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_3 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking re-acquired", assertMidSliderJudgements);
}
/// <summary>
/// Scenario:
/// - Press a key before a slider starts
/// - Press the other key on the slider head timed correctly while holding the original key
/// - Release the key used to hit the slider head
/// - While holding the first key, move the cursor away from the slider body
/// - Still holding the first key, move the cursor back to the slider body
/// Expected Result:
/// A passing test case will have the slider not track despite having the cursor return to the slider body.
/// </summary>
[Test]
public void TestTrackingReturnMidSliderKeyDownBefore()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
new OsuReplayFrame { Position = new Vector2(200, 200), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_3 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking lost", assertMidSliderJudgementFail);
}
/// <summary>
/// Scenario:
/// - Wait for the slider to reach a mid-point
/// - Press a key away from the slider body
/// - While holding down the key, move into the slider body
/// Expected Result:
/// A passing test case will have the slider track the cursor after the cursor enters the slider body.
/// </summary>
[Test]
public void TestTrackingMidSlider()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(150, 150), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
new OsuReplayFrame { Position = new Vector2(200, 200), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_3 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
}
/// <summary>
/// Scenario:
/// - Press a key before the slider starts
/// - Press another key on the slider head while holding the original key
/// - Move out of the slider body while releasing the two pressed keys
/// - Move back into the slider body while pressing any key.
/// Expected Result:
/// A passing test case will have the slider track the cursor after the cursor enters the slider body.
/// </summary>
[Test]
public void TestMidSliderTrackingAcquired()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(100, 100), Time = time_during_slide_1 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
}
[Test]
public void TestMidSliderTrackingAcquiredWithMouseDownOutsideSlider()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(100, 100), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
}
/// <summary>
/// Scenario:
/// - Press a key on the slider head
/// - While holding the key, move outside of the slider body with the cursor
/// - Release the key while outside of the slider body
/// - Press the key again while outside of the slider body
/// - Move back into the slider body while holding the pressed key
/// Expected Result:
/// A passing test case will have the slider track the cursor after the cursor enters the slider body.
/// </summary>
[Test]
public void TestTrackingReleasedValidKey()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(100, 100), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
new OsuReplayFrame { Position = new Vector2(100, 100), Time = time_during_slide_2 },
new OsuReplayFrame { Position = new Vector2(100, 100), Actions = { OsuAction.LeftButton }, Time = time_during_slide_3 },
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
}
private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great;
private bool assertHeadMissTailTracked() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss;
private bool assertMidSliderJudgements() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great;
private bool assertMidSliderJudgementFail() => judgementResults[judgementResults.Count - 2].Type == HitResult.Miss;
private ScoreAccessibleReplayPlayer currentPlayer;
private void performTest(List<ReplayFrame> frames)
{
// Empty frame to be added as a workaround for first frame behavior.
// If an input exists on the first frame, the input would apply to the entire intro lead-in
// Likely requires some discussion regarding how first frame inputs should be handled.
frames.Insert(0, new OsuReplayFrame());
AddStep("load player", () =>
{
Beatmap.Value = new TestWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new Slider
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(25, 0),
}, 25),
}
},
ControlPointInfo =
{
DifficultyPoints = { new DifficultyControlPoint { SpeedMultiplier = 0.1f } }
},
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo
},
}, Clock);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } })
{
AllowPause = false,
AllowLeadIn = false,
AllowResults = false
};
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
};
LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>();
});
AddUntilStep(() => Beatmap.Value.Track.CurrentTime == 0, "Beatmap at 0");
AddUntilStep(() => currentPlayer.IsCurrentScreen(), "Wait until player is loaded");
AddUntilStep(() => allJudgedFired, "Wait for all judged");
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public ScoreAccessibleReplayPlayer(Score score)
: base(score)
{
}
}
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public class TestCaseSpinner : OsuTestCase public class TestCaseSpinner : OsuTestCase
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(SpinnerDisc), typeof(SpinnerDisc),
typeof(DrawableSpinner), typeof(DrawableSpinner),
typeof(DrawableOsuHitObject) typeof(DrawableOsuHitObject)
@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
private bool auto; private bool auto;
public TestDrawableSpinner(Spinner s, bool auto) : base(s) public TestDrawableSpinner(Spinner s, bool auto)
: base(s)
{ {
this.auto = auto; this.auto = auto;
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Difficulty namespace osu.Game.Rulesets.Osu.Difficulty
{ {
@ -13,10 +12,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double ApproachRate; public double ApproachRate;
public double OverallDifficulty; public double OverallDifficulty;
public int MaxCombo; public int MaxCombo;
public OsuDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)
{
}
} }
} }

View File

@ -1,10 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Difficulty.Skills;
@ -15,7 +18,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuDifficultyCalculator : DifficultyCalculator public class OsuDifficultyCalculator : DifficultyCalculator
{ {
private const int section_length = 400;
private const double difficulty_multiplier = 0.0675; private const double difficulty_multiplier = 0.0675;
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
@ -23,58 +25,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
} }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
if (!beatmap.HitObjects.Any()) if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes(mods, 0); return new OsuDifficultyAttributes { Mods = mods };
OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast<OsuHitObject>().ToList(), timeRate);
Skill[] skills =
{
new Aim(),
new Speed()
};
double sectionLength = section_length * timeRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
foreach (OsuDifficultyHitObject h in difficultyBeatmap)
{
while (h.BaseObject.StartTime > currentSectionEnd)
{
foreach (Skill s in skills)
{
s.SaveCurrentPeak();
s.StartNewSectionFrom(currentSectionEnd);
}
currentSectionEnd += sectionLength;
}
foreach (Skill s in skills)
s.Process(h);
}
// The peak strain will not be saved for the last section in the above loop
foreach (Skill s in skills)
s.SaveCurrentPeak();
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2; double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate; double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate; double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
int maxCombo = beatmap.HitObjects.Count; 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) // 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); maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
return new OsuDifficultyAttributes(mods, starRating) return new OsuDifficultyAttributes
{ {
StarRating = starRating,
Mods = mods,
AimStrain = aimRating, AimStrain = aimRating,
SpeedStrain = speedRating, SpeedStrain = speedRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
@ -83,6 +54,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}; };
} }
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
// The first jump is formed by the first two hitobjects of the map.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < beatmap.HitObjects.Count; i++)
{
var lastLast = i > 1 ? beatmap.HitObjects[i - 2] : null;
var last = beatmap.HitObjects[i - 1];
var current = beatmap.HitObjects[i];
yield return new OsuDifficultyHitObject(current, lastLast, last, clockRate);
}
}
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{
new Aim(),
new Speed()
};
protected override Mod[] DifficultyAdjustmentMods => new Mod[] protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{ {
new OsuModDoubleTime(), new OsuModDoubleTime(),

View File

@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Longer maps are worth more // Longer maps are worth more
double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
aimValue *= lengthBonus; aimValue *= lengthBonus;
@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f); approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f);
else if (Attributes.ApproachRate < 8.0f) else if (Attributes.ApproachRate < 8.0f)
{ {
approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate); approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate);
} }
aimValue *= approachRateFactor; aimValue *= approachRateFactor;
@ -126,8 +126,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
// Apply object-based bonus for flashlight. // Apply object-based bonus for flashlight.
aimValue *= 1.0f + 0.35f * Math.Min(1.0f, totalHits / 200.0f) + aimValue *= 1.0f + 0.35f * Math.Min(1.0f, totalHits / 200.0f) +
(totalHits > 200 ? 0.3f * Math.Min(1.0f, (totalHits - 200) / 300.0f) + (totalHits > 200
(totalHits > 500 ? (totalHits - 500) / 1200.0f : 0.0f) : 0.0f); ? 0.3f * Math.Min(1.0f, (totalHits - 200) / 300.0f) +
(totalHits > 500 ? (totalHits - 500) / 1200.0f : 0.0f)
: 0.0f);
} }
// Scale the aim value with accuracy _slightly_ // Scale the aim value with accuracy _slightly_
@ -144,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Longer maps are worth more // Longer maps are worth more
speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
speedValue *= Math.Pow(0.97f, countMiss); speedValue *= Math.Pow(0.97f, countMiss);

View File

@ -1,50 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
/// <summary>
/// An enumerable container wrapping <see cref="OsuHitObject"/> input as <see cref="OsuDifficultyHitObject"/>
/// which contains extra data required for difficulty calculation.
/// </summary>
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
{
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
/// <summary>
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
difficultyObjects = createDifficultyObjectEnumerator(objects.OrderBy(h => h.StartTime).ToList(), timeRate);
}
/// <summary>
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
/// </summary>
public IEnumerator<OsuDifficultyHitObject> GetEnumerator() => difficultyObjects;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
{
// The first jump is formed by the first two hitobjects of the map.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; i++)
{
var lastLast = i > 1 ? objects[i - 2] : null;
var last = objects[i - 1];
var current = objects[i];
yield return new OsuDifficultyHitObject(lastLast, last, current, timeRate);
}
}
}
}

View File

@ -1,24 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
/// <summary> public class OsuDifficultyHitObject : DifficultyHitObject
/// A wrapper around <see cref="OsuHitObject"/> extending it with additional data required for difficulty calculation.
/// </summary>
public class OsuDifficultyHitObject
{ {
private const int normalized_radius = 52; private const int normalized_radius = 52;
/// <summary> protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
/// The <see cref="OsuHitObject"/> this <see cref="OsuDifficultyHitObject"/> refers to.
/// </summary>
public OsuHitObject BaseObject { get; }
/// <summary> /// <summary>
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>. /// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
@ -30,40 +26,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary> /// </summary>
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
/// <summary>
/// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
/// </summary>
public double StrainTime { get; private set; }
/// <summary> /// <summary>
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>. /// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
/// Calculated as the angle between the circles (current-2, current-1, current). /// Calculated as the angle between the circles (current-2, current-1, current).
/// </summary> /// </summary>
public double? Angle { get; private set; } public double? Angle { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
/// </summary>
public readonly double StrainTime;
private readonly OsuHitObject lastLastObject; private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject; private readonly OsuHitObject lastObject;
private readonly double timeRate;
/// <summary> public OsuDifficultyHitObject(HitObject hitObject, HitObject lastLastObject, HitObject lastObject, double clockRate)
/// Initializes the object calculating extra data required for difficulty calculation. : base(hitObject, lastObject, clockRate)
/// </summary>
public OsuDifficultyHitObject(OsuHitObject lastLastObject, OsuHitObject lastObject, OsuHitObject currentObject, double timeRate)
{ {
this.lastLastObject = lastLastObject; this.lastLastObject = (OsuHitObject)lastLastObject;
this.lastObject = lastObject; this.lastObject = (OsuHitObject)lastObject;
this.timeRate = timeRate;
BaseObject = currentObject;
setDistances(); setDistances();
setTimingValues();
// Calculate angle here // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(50, DeltaTime);
} }
private void setDistances() private void setDistances()
@ -102,18 +88,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
} }
} }
private void setTimingValues()
{
DeltaTime = (BaseObject.StartTime - lastObject.StartTime) / timeRate;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(50, DeltaTime);
}
private void computeSliderCursorPosition(Slider slider) private void computeSliderCursorPosition(Slider slider)
{ {
if (slider.LazyEndPosition != null) if (slider.LazyEndPosition != null)
return; return;
slider.LazyEndPosition = slider.StackedPosition; slider.LazyEndPosition = slider.StackedPosition;
float approxFollowCircleRadius = (float)(slider.Radius * 3); float approxFollowCircleRadius = (float)(slider.Radius * 3);
@ -149,8 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
Vector2 pos = hitObject.StackedPosition; Vector2 pos = hitObject.StackedPosition;
var slider = hitObject as Slider; if (hitObject is Slider slider)
if (slider != null)
{ {
computeSliderCursorPosition(slider); computeSliderCursorPosition(slider);
pos = slider.LazyEndPosition ?? pos; pos = slider.LazyEndPosition ?? pos;

View File

@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
@ -17,33 +20,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double SkillMultiplier => 26.25; protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15; protected override double StrainDecayBase => 0.15;
protected override double StrainValueOf(OsuDifficultyHitObject current) protected override double StrainValueOf(DifficultyHitObject current)
{ {
if (current.BaseObject is Spinner)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
double result = 0; double result = 0;
const double scale = 90;
double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
if (Previous.Count > 0) if (Previous.Count > 0)
{ {
if (current.Angle != null && current.Angle.Value > angle_bonus_begin) var osuPrevious = (OsuDifficultyHitObject)Previous[0];
if (osuCurrent.Angle != null && osuCurrent.Angle.Value > angle_bonus_begin)
{ {
const double scale = 90;
var angleBonus = Math.Sqrt( var angleBonus = Math.Sqrt(
Math.Max(Previous[0].JumpDistance - scale, 0) Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(current.Angle.Value - angle_bonus_begin), 2) * Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(current.JumpDistance - scale, 0)); * Math.Max(osuCurrent.JumpDistance - scale, 0));
result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, Previous[0].StrainTime); result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
} }
} }
double jumpDistanceExp = applyDiminishingExp(current.JumpDistance); double jumpDistanceExp = applyDiminishingExp(osuCurrent.JumpDistance);
double travelDistanceExp = applyDiminishingExp(current.TravelDistance); double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
return Math.Max( return Math.Max(
result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(current.StrainTime, timing_threshold), result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / current.StrainTime (Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
); );
} }
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
} }
} }

View File

@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
@ -11,6 +14,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary> /// </summary>
public class Speed : Skill public class Speed : Skill
{ {
private const double single_spacing_threshold = 125;
private const double angle_bonus_begin = 5 * Math.PI / 6; private const double angle_bonus_begin = 5 * Math.PI / 6;
private const double pi_over_4 = Math.PI / 4; private const double pi_over_4 = Math.PI / 4;
private const double pi_over_2 = Math.PI / 2; private const double pi_over_2 = Math.PI / 2;
@ -22,9 +27,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private const double max_speed_bonus = 45; // ~330BPM private const double max_speed_bonus = 45; // ~330BPM
private const double speed_balancing_factor = 40; private const double speed_balancing_factor = 40;
protected override double StrainValueOf(OsuDifficultyHitObject current) protected override double StrainValueOf(DifficultyHitObject current)
{ {
double distance = Math.Min(SINGLE_SPACING_THRESHOLD, current.TravelDistance + current.JumpDistance); if (current.BaseObject is Spinner)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime);
double speedBonus = 1.0; double speedBonus = 1.0;
@ -32,20 +42,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2);
double angleBonus = 1.0; double angleBonus = 1.0;
if (current.Angle != null && current.Angle.Value < angle_bonus_begin) if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin)
{ {
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - current.Angle.Value)), 2) / 3.57; angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
if (current.Angle.Value < pi_over_2) if (osuCurrent.Angle.Value < pi_over_2)
{ {
angleBonus = 1.28; angleBonus = 1.28;
if (distance < 90 && current.Angle.Value < pi_over_4) if (distance < 90 && osuCurrent.Angle.Value < pi_over_4)
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1); angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
else if (distance < 90) else if (distance < 90)
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - current.Angle.Value) / pi_over_4); angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4);
} }
} }
return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / SINGLE_SPACING_THRESHOLD, 3.5)) / current.StrainTime; return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime;
} }
} }
} }

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
PositionBindable.BindValueChanged(_ => UpdatePosition(), true); PositionBindable.BindValueChanged(_ => UpdatePosition(), true);
StackHeightBindable.BindValueChanged(_ => UpdatePosition()); StackHeightBindable.BindValueChanged(_ => UpdatePosition());
ScaleBindable.BindValueChanged(v => Scale = new Vector2(v), true); ScaleBindable.BindValueChanged(scale => Scale = new Vector2(scale.NewValue), true);
} }
protected virtual void UpdatePosition() => Position = hitCircle.StackedPosition; protected virtual void UpdatePosition() => Position = hitCircle.StackedPosition;

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
body.BorderColour = colours.Yellow; body.BorderColour = colours.Yellow;
PositionBindable.BindValueChanged(_ => updatePosition(), true); PositionBindable.BindValueChanged(_ => updatePosition(), true);
ScaleBindable.BindValueChanged(v => body.PathWidth = v * 64, true); ScaleBindable.BindValueChanged(scale => body.PathWidth = scale.NewValue * 64, true);
} }
private void updatePosition() => Position = slider.StackedPosition; private void updatePosition() => Position = slider.StackedPosition;

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
PositionBindable.BindValueChanged(_ => updatePosition(), true); PositionBindable.BindValueChanged(_ => updatePosition(), true);
StackHeightBindable.BindValueChanged(_ => updatePosition()); StackHeightBindable.BindValueChanged(_ => updatePosition());
ScaleBindable.BindValueChanged(v => ring.Scale = new Vector2(v), true); ScaleBindable.BindValueChanged(scale => ring.Scale = new Vector2(scale.NewValue), true);
} }
private void updatePosition() => Position = spinner.Position; private void updatePosition() => Position = spinner.Position;

View File

@ -9,8 +9,10 @@ namespace osu.Game.Rulesets.Osu.Judgements
{ {
[Description(@"")] [Description(@"")]
None, None,
[Description(@"Good")] [Description(@"Good")]
Good, Good,
[Description(@"Amazing")] [Description(@"Amazing")]
Perfect Perfect
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{ {
scoreProcessor.Health.ValueChanged += val => { blinds.AnimateClosedness((float)val); }; scoreProcessor.Health.ValueChanged += health => { blinds.AnimateClosedness((float)health.NewValue); };
} }
/// <summary> /// <summary>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -41,9 +42,9 @@ namespace osu.Game.Rulesets.Osu.Mods
return default_flashlight_size; return default_flashlight_size;
} }
protected override void OnComboChange(int newCombo) protected override void OnComboChange(ValueChangedEvent<int> e)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(newCombo)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -0,0 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModGrow : Mod, IApplicableToDrawableHitObjects
{
public override string Name => "Grow";
public override string Acronym => "GR";
public override FontAwesome Icon => FontAwesome.fa_arrows_v;
public override ModType Type => ModType.Fun;
public override string Description => "Hit them at the right size!";
public override double ScoreMultiplier => 1;
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var drawable in drawables)
{
switch (drawable)
{
case DrawableSpinner _:
continue;
default:
drawable.ApplyCustomUpdateState += ApplyCustomState;
break;
}
}
}
protected virtual void ApplyCustomState(DrawableHitObject drawable, ArmedState state)
{
var h = (OsuHitObject)drawable.HitObject;
// apply grow effect
switch (drawable)
{
case DrawableSliderHead _:
case DrawableSliderTail _:
// special cases we should *not* be scaling.
break;
case DrawableSlider _:
case DrawableHitCircle _:
{
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
drawable.ScaleTo(0.5f).Then().ScaleTo(1, h.TimePreempt, Easing.OutSine);
break;
}
}
// remove approach circles
switch (drawable)
{
case DrawableHitCircle circle:
// we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide();
break;
}
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
foreach (var drawable in drawables) foreach (var drawable in drawables)
{ {
var hitObject = (OsuHitObject) drawable.HitObject; var hitObject = (OsuHitObject)drawable.HitObject;
float appearDistance = (float)(hitObject.TimePreempt - hitObject.TimeFadeIn) / 2; float appearDistance = (float)(hitObject.TimePreempt - hitObject.TimeFadeIn) / 2;
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Mods
.MoveTo(originalPosition, moveDuration, Easing.InOutSine); .MoveTo(originalPosition, moveDuration, Easing.InOutSine);
} }
theta += (float) hitObject.TimeFadeIn / 1000; theta += (float)hitObject.TimeFadeIn / 1000;
} }
} }
} }

View File

@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// Connects hit objects visually, for example with follow points. /// Connects hit objects visually, for example with follow points.
/// </summary> /// </summary>
public abstract class ConnectionRenderer<T> : Container public abstract class ConnectionRenderer<T> : LifetimeManagementContainer
where T : HitObject where T : HitObject
{ {
/// <summary> /// <summary>

View File

@ -12,39 +12,44 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
public class FollowPointRenderer : ConnectionRenderer<OsuHitObject> public class FollowPointRenderer : ConnectionRenderer<OsuHitObject>
{ {
private int pointDistance = 32; private int pointDistance = 32;
/// <summary> /// <summary>
/// Determines how much space there is between points. /// Determines how much space there is between points.
/// </summary> /// </summary>
public int PointDistance public int PointDistance
{ {
get { return pointDistance; } get => pointDistance;
set set
{ {
if (pointDistance == value) return; if (pointDistance == value) return;
pointDistance = value; pointDistance = value;
update(); update();
} }
} }
private int preEmpt = 800; private int preEmpt = 800;
/// <summary> /// <summary>
/// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time. /// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time.
/// </summary> /// </summary>
public int PreEmpt public int PreEmpt
{ {
get { return preEmpt; } get => preEmpt;
set set
{ {
if (preEmpt == value) return; if (preEmpt == value) return;
preEmpt = value; preEmpt = value;
update(); update();
} }
} }
private IEnumerable<OsuHitObject> hitObjects; private IEnumerable<OsuHitObject> hitObjects;
public override IEnumerable<OsuHitObject> HitObjects public override IEnumerable<OsuHitObject> HitObjects
{ {
get { return hitObjects; } get => hitObjects;
set set
{ {
hitObjects = value; hitObjects = value;
@ -56,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void update() private void update()
{ {
Clear(); ClearInternal();
if (hitObjects == null) if (hitObjects == null)
return; return;
@ -86,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
FollowPoint fp; FollowPoint fp;
Add(fp = new FollowPoint AddInternal(fp = new FollowPoint
{ {
Position = pointStartPosition, Position = pointStartPosition,
Rotation = rotation, Rotation = rotation,
@ -107,6 +112,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
fp.Expire(true); fp.Expire(true);
} }
} }
prevHitObject = currHitObject; prevHitObject = currHitObject;
} }
} }

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