mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 16:27:26 +08:00
Merge branch 'master' into MissingSource
This commit is contained in:
commit
97dc7f9819
8
.github/pull_request_template.md
vendored
8
.github/pull_request_template.md
vendored
@ -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.
|
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -11,8 +11,9 @@
|
|||||||
*.userprefs
|
*.userprefs
|
||||||
|
|
||||||
### Cake ###
|
### Cake ###
|
||||||
tools/*
|
tools/**
|
||||||
!tools/cakebuild.csproj
|
build/tools/**
|
||||||
|
|
||||||
|
|
||||||
# Build results
|
# Build results
|
||||||
bin/[Dd]ebug/
|
bin/[Dd]ebug/
|
||||||
|
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
13
build.ps1
13
build.ps1
@ -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
|
||||||
|
3
build.sh
3
build.sh
@ -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=()
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -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
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
|
||||||
|
|
||||||
[TestCase(3.8664391043534758, "diffcalc-test")]
|
[TestCase(4.2038001515546597d, "diffcalc-test")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,148 +1,89 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
Normal file
85
osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
||||||
|
|
||||||
[TestCase(2.2676066895468976, "diffcalc-test")]
|
[TestCase(2.3683365342338796d, "diffcalc-test")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
Normal file
47
osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
Normal file
56
osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,11 +76,11 @@ 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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,11 +32,11 @@ 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
|
||||||
|
@ -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,9 +49,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,15 +82,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +156,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 +167,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 =
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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;
|
||||||
@ -76,7 +76,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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
@ -313,7 +314,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)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
372
osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs
Normal file
372
osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,14 +88,6 @@ 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)
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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";
|
||||||
|
74
osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
Normal file
74
osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
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.Containers;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -27,40 +28,60 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
private readonly IBindable<int> stackHeightBindable = new Bindable<int>();
|
private readonly IBindable<int> stackHeightBindable = new Bindable<int>();
|
||||||
private readonly IBindable<float> scaleBindable = new Bindable<float>();
|
private readonly IBindable<float> scaleBindable = new Bindable<float>();
|
||||||
|
|
||||||
|
public OsuAction? HitAction => circle.HitAction;
|
||||||
|
|
||||||
|
private readonly Container explodeContainer;
|
||||||
|
|
||||||
|
private readonly Container scaleContainer;
|
||||||
|
|
||||||
public DrawableHitCircle(HitCircle h)
|
public DrawableHitCircle(HitCircle h)
|
||||||
: base(h)
|
: base(h)
|
||||||
{
|
{
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
Position = HitObject.StackedPosition;
|
Position = HitObject.StackedPosition;
|
||||||
Scale = new Vector2(h.Scale);
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
glow = new GlowPiece(),
|
scaleContainer = new Container
|
||||||
circle = new CirclePiece
|
|
||||||
{
|
{
|
||||||
Hit = () =>
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Child = explodeContainer = new Container
|
||||||
{
|
{
|
||||||
if (AllJudged)
|
RelativeSizeAxes = Axes.Both,
|
||||||
return false;
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
glow = new GlowPiece(),
|
||||||
|
circle = new CirclePiece
|
||||||
|
{
|
||||||
|
Hit = () =>
|
||||||
|
{
|
||||||
|
if (AllJudged)
|
||||||
|
return false;
|
||||||
|
|
||||||
UpdateResult(true);
|
UpdateResult(true);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
number = new NumberPiece
|
||||||
|
{
|
||||||
|
Text = (HitObject.IndexInCurrentCombo + 1).ToString(),
|
||||||
|
},
|
||||||
|
ring = new RingPiece(),
|
||||||
|
flash = new FlashPiece(),
|
||||||
|
explode = new ExplodePiece(),
|
||||||
|
ApproachCircle = new ApproachCircle
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
Scale = new Vector2(4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
number = new NumberPiece
|
|
||||||
{
|
|
||||||
Text = (HitObject.IndexInCurrentCombo + 1).ToString(),
|
|
||||||
},
|
|
||||||
ring = new RingPiece(),
|
|
||||||
flash = new FlashPiece(),
|
|
||||||
explode = new ExplodePiece(),
|
|
||||||
ApproachCircle = new ApproachCircle
|
|
||||||
{
|
|
||||||
Alpha = 0,
|
|
||||||
Scale = new Vector2(4),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//may not be so correct
|
//may not be so correct
|
||||||
@ -72,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||||
stackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
stackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||||
scaleBindable.BindValueChanged(v => Scale = new Vector2(v));
|
scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
|
||||||
|
|
||||||
positionBindable.BindTo(HitObject.PositionBindable);
|
positionBindable.BindTo(HitObject.PositionBindable);
|
||||||
stackHeightBindable.BindTo(HitObject.StackHeightBindable);
|
stackHeightBindable.BindTo(HitObject.StackHeightBindable);
|
||||||
@ -131,6 +152,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
Expire(true);
|
Expire(true);
|
||||||
|
|
||||||
|
circle.HitAction = null;
|
||||||
|
|
||||||
// override lifetime end as FadeIn may have been changed externally, causing out expiration to be too early.
|
// override lifetime end as FadeIn may have been changed externally, causing out expiration to be too early.
|
||||||
LifetimeEnd = HitObject.StartTime + HitObject.HitWindows.HalfWindowFor(HitResult.Miss);
|
LifetimeEnd = HitObject.StartTime + HitObject.HitWindows.HalfWindowFor(HitResult.Miss);
|
||||||
break;
|
break;
|
||||||
@ -156,8 +179,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
circle.FadeOut();
|
circle.FadeOut();
|
||||||
number.FadeOut();
|
number.FadeOut();
|
||||||
|
|
||||||
this.FadeOut(800)
|
this.FadeOut(800);
|
||||||
.ScaleTo(Scale * 1.5f, 400, Easing.OutQuad);
|
explodeContainer.ScaleTo(1.5f, 400, Easing.OutQuad);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expire();
|
Expire();
|
||||||
|
@ -8,7 +8,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
|||||||
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.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both },
|
repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both },
|
||||||
Ball = new SliderBall(s, this)
|
Ball = new SliderBall(s, this)
|
||||||
{
|
{
|
||||||
|
GetInitialHitAction = () => HeadCircle.HitAction,
|
||||||
BypassAutoSizeAxes = Axes.Both,
|
BypassAutoSizeAxes = Axes.Both,
|
||||||
Scale = new Vector2(s.Scale),
|
Scale = new Vector2(s.Scale),
|
||||||
AlwaysPresent = true,
|
AlwaysPresent = true,
|
||||||
@ -99,10 +100,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
config.BindWith(OsuSetting.SnakingOutSliders, Body.SnakingOut);
|
config.BindWith(OsuSetting.SnakingOutSliders, Body.SnakingOut);
|
||||||
|
|
||||||
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
|
||||||
scaleBindable.BindValueChanged(v =>
|
scaleBindable.BindValueChanged(scale =>
|
||||||
{
|
{
|
||||||
Body.PathWidth = HitObject.Scale * 64;
|
Body.PathWidth = scale.NewValue * 64;
|
||||||
Ball.Scale = new Vector2(HitObject.Scale);
|
Ball.Scale = new Vector2(scale.NewValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
positionBindable.BindTo(HitObject.PositionBindable);
|
positionBindable.BindTo(HitObject.PositionBindable);
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
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.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
@ -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.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
@ -11,7 +11,7 @@ using osuTK.Graphics;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Configuration;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
circle.Colour = colours.BlueDark;
|
circle.Colour = colours.BlueDark;
|
||||||
glow.Colour = colours.BlueDark;
|
glow.Colour = colours.BlueDark;
|
||||||
|
|
||||||
positionBindable.BindValueChanged(v => Position = v);
|
positionBindable.BindValueChanged(pos => Position = pos.NewValue);
|
||||||
positionBindable.BindTo(HitObject.PositionBindable);
|
positionBindable.BindTo(HitObject.PositionBindable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
|
|
||||||
public Func<bool> Hit;
|
public Func<bool> Hit;
|
||||||
|
|
||||||
|
public OsuAction? HitAction;
|
||||||
|
|
||||||
public CirclePiece()
|
public CirclePiece()
|
||||||
{
|
{
|
||||||
Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2);
|
Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2);
|
||||||
@ -35,7 +37,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
{
|
{
|
||||||
case OsuAction.LeftButton:
|
case OsuAction.LeftButton:
|
||||||
case OsuAction.RightButton:
|
case OsuAction.RightButton:
|
||||||
return IsHovered && (Hit?.Invoke() ?? false);
|
if (IsHovered && (Hit?.Invoke() ?? false))
|
||||||
|
{
|
||||||
|
HitAction = action;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||||
@ -42,9 +43,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
}, s => s.GetTexture("Play/osu/hitcircle") == null),
|
}, s => s.GetTexture("Play/osu/hitcircle") == null),
|
||||||
number = new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
|
number = new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
|
||||||
{
|
{
|
||||||
Font = @"Venera",
|
Font = OsuFont.Numeric.With(size: 40),
|
||||||
UseFullGlyphHeight = false,
|
UseFullGlyphHeight = false,
|
||||||
TextSize = 40,
|
|
||||||
}, restrictSize: false)
|
}, restrictSize: false)
|
||||||
{
|
{
|
||||||
Text = @"1"
|
Text = @"1"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
|
|
||||||
private Color4 accentColour = Color4.Black;
|
private Color4 accentColour = Color4.Black;
|
||||||
|
|
||||||
|
public Func<OsuAction?> GetInitialHitAction;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The colour that is used for the slider ball.
|
/// The colour that is used for the slider ball.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -145,20 +148,72 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool canCurrentlyTrack => Time.Current >= slider.StartTime && Time.Current < slider.EndTime;
|
/// <summary>
|
||||||
|
/// If the cursor moves out of the ball's radius we still need to be able to receive positional updates to stop tracking.
|
||||||
|
/// </summary>
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
|
||||||
|
///
|
||||||
|
/// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders.
|
||||||
|
/// Visually, this special case can be seen below (time increasing from left to right):
|
||||||
|
///
|
||||||
|
/// Z Z+X Z
|
||||||
|
/// o========o
|
||||||
|
///
|
||||||
|
/// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it.
|
||||||
|
///
|
||||||
|
/// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking).
|
||||||
|
///
|
||||||
|
/// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios.
|
||||||
|
/// </summary>
|
||||||
|
private double? timeToAcceptAnyKeyAfter;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (Time.Current < slider.EndTime)
|
// from the point at which the head circle is hit, this will be non-null.
|
||||||
|
// it may be null if the head circle was missed.
|
||||||
|
var headCircleHitAction = GetInitialHitAction();
|
||||||
|
|
||||||
|
if (headCircleHitAction == null)
|
||||||
|
timeToAcceptAnyKeyAfter = null;
|
||||||
|
|
||||||
|
var actions = drawableSlider?.OsuActionInputManager?.PressedActions;
|
||||||
|
|
||||||
|
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
|
||||||
|
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
|
||||||
{
|
{
|
||||||
// Make sure to use the base version of ReceivePositionalInputAt so that we correctly check the position.
|
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
|
||||||
Tracking = canCurrentlyTrack
|
|
||||||
&& lastScreenSpaceMousePosition.HasValue
|
// we can return to accepting all keys if the initial head circle key is the *only* key pressed, or all keys have been released.
|
||||||
&& ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value)
|
if (actions?.Contains(otherKey) != true)
|
||||||
&& (drawableSlider?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
|
timeToAcceptAnyKeyAfter = Time.Current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Tracking =
|
||||||
|
// in valid time range
|
||||||
|
Time.Current >= slider.StartTime && Time.Current < slider.EndTime &&
|
||||||
|
// in valid position range
|
||||||
|
lastScreenSpaceMousePosition.HasValue && base.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
|
||||||
|
// valid action
|
||||||
|
(actions?.Any(isValidTrackingAction) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether a given user input is a valid tracking action.
|
||||||
|
/// </summary>
|
||||||
|
private bool isValidTrackingAction(OsuAction action)
|
||||||
|
{
|
||||||
|
bool headCircleHit = GetInitialHitAction().HasValue;
|
||||||
|
|
||||||
|
// if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action.
|
||||||
|
if (headCircleHit && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value))
|
||||||
|
return action == GetInitialHitAction();
|
||||||
|
|
||||||
|
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateProgress(double completionProgress)
|
public void UpdateProgress(double completionProgress)
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Configuration;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -54,18 +54,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
var spanProgress = slider.ProgressAt(completionProgress);
|
var spanProgress = slider.ProgressAt(completionProgress);
|
||||||
|
|
||||||
double start = 0;
|
double start = 0;
|
||||||
double end = SnakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadeIn, 0, 1) : 1;
|
double end = SnakingIn.Value ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadeIn, 0, 1) : 1;
|
||||||
|
|
||||||
if (span >= slider.SpanCount() - 1)
|
if (span >= slider.SpanCount() - 1)
|
||||||
{
|
{
|
||||||
if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1)
|
if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1)
|
||||||
{
|
{
|
||||||
start = 0;
|
start = 0;
|
||||||
end = SnakingOut ? spanProgress : 1;
|
end = SnakingOut.Value ? spanProgress : 1;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
start = SnakingOut ? spanProgress : 0;
|
start = SnakingOut.Value ? spanProgress : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||||
@ -23,16 +24,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
|||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
Text = @"0",
|
Text = @"0",
|
||||||
Font = @"Venera",
|
Font = OsuFont.Numeric.With(size: 24)
|
||||||
TextSize = 24
|
|
||||||
},
|
},
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
Text = @"SPINS PER MINUTE",
|
Text = @"SPINS PER MINUTE",
|
||||||
Font = @"Venera",
|
Font = OsuFont.Numeric.With(size: 12),
|
||||||
TextSize = 12,
|
|
||||||
Y = 30
|
Y = 30
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
public virtual Vector2 Position
|
public virtual Vector2 Position
|
||||||
{
|
{
|
||||||
get => PositionBindable;
|
get => PositionBindable.Value;
|
||||||
set => PositionBindable.Value = value;
|
set => PositionBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
public int StackHeight
|
public int StackHeight
|
||||||
{
|
{
|
||||||
get => StackHeightBindable;
|
get => StackHeightBindable.Value;
|
||||||
set => StackHeightBindable.Value = value;
|
set => StackHeightBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
public float Scale
|
public float Scale
|
||||||
{
|
{
|
||||||
get => ScaleBindable;
|
get => ScaleBindable.Value;
|
||||||
set => ScaleBindable.Value = value;
|
set => ScaleBindable.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ using osu.Game.Rulesets.Objects.Types;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Configuration;
|
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
@ -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.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
|
@ -124,6 +124,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
return new Mod[] {
|
return new Mod[] {
|
||||||
new OsuModTransform(),
|
new OsuModTransform(),
|
||||||
new OsuModWiggle(),
|
new OsuModWiggle(),
|
||||||
|
new OsuModGrow()
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return new Mod[] { };
|
return new Mod[] { };
|
||||||
|
@ -11,9 +11,9 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Replays
|
namespace osu.Game.Rulesets.Osu.Replays
|
||||||
{
|
{
|
||||||
public class OsuReplayInputHandler : FramedReplayInputHandler<OsuReplayFrame>
|
public class OsuFramedReplayInputHandler : FramedReplayInputHandler<OsuReplayFrame>
|
||||||
{
|
{
|
||||||
public OsuReplayInputHandler(Replay replay)
|
public OsuFramedReplayInputHandler(Replay replay)
|
||||||
: base(replay)
|
: base(replay)
|
||||||
{
|
{
|
||||||
}
|
}
|
@ -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;
|
||||||
@ -183,13 +183,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.beatmap.BindTo(beatmap);
|
this.beatmap.BindTo(beatmap);
|
||||||
this.beatmap.ValueChanged += v => calculateScale();
|
this.beatmap.ValueChanged += _ => calculateScale();
|
||||||
|
|
||||||
cursorScale = config.GetBindable<double>(OsuSetting.GameplayCursorSize);
|
cursorScale = config.GetBindable<double>(OsuSetting.GameplayCursorSize);
|
||||||
cursorScale.ValueChanged += v => calculateScale();
|
cursorScale.ValueChanged += _ => calculateScale();
|
||||||
|
|
||||||
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
|
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
|
||||||
autoCursorScale.ValueChanged += v => calculateScale();
|
autoCursorScale.ValueChanged += _ => calculateScale();
|
||||||
|
|
||||||
calculateScale();
|
calculateScale();
|
||||||
}
|
}
|
||||||
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
{
|
{
|
||||||
float scale = (float)cursorScale.Value;
|
float scale = (float)cursorScale.Value;
|
||||||
|
|
||||||
if (autoCursorScale && beatmap.Value != null)
|
if (autoCursorScale.Value && beatmap.Value != null)
|
||||||
{
|
{
|
||||||
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
|
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
|
||||||
scale *= (float)(1 - 0.7 * (1 + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY);
|
scale *= (float)(1 - 0.7 * (1 + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY);
|
||||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||||
{
|
{
|
||||||
if (!judgedObject.DisplayResult || !DisplayJudgements)
|
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject)
|
DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject)
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay);
|
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay);
|
||||||
|
|
||||||
public override double GameplayStartTime
|
public override double GameplayStartTime
|
||||||
{
|
{
|
||||||
|
@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(2.9811336589467095, "diffcalc-test")]
|
[TestCase(2.9811338051242915d, "diffcalc-test")]
|
||||||
[TestCase(2.9811336589467095, "diffcalc-test-strong")]
|
[TestCase(2.9811338051242915d, "diffcalc-test-strong")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
Ruleset = new TaikoRuleset().RulesetInfo
|
Ruleset = new TaikoRuleset().RulesetInfo
|
||||||
},
|
},
|
||||||
ControlPointInfo = controlPointInfo
|
ControlPointInfo = controlPointInfo
|
||||||
});
|
}, Clock);
|
||||||
|
|
||||||
Add(playfieldContainer = new Container
|
Add(playfieldContainer = new Container
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
// 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.Objects;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
public class TaikoDifficultyHitObject : DifficultyHitObject
|
||||||
|
{
|
||||||
|
public readonly bool HasTypeChange;
|
||||||
|
|
||||||
|
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
|
||||||
|
: base(hitObject, lastObject, clockRate)
|
||||||
|
{
|
||||||
|
HasTypeChange = lastObject is RimHit != hitObject is RimHit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
Normal file
95
osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
public class Strain : Skill
|
||||||
|
{
|
||||||
|
private const double rhythm_change_base_threshold = 0.2;
|
||||||
|
private const double rhythm_change_base = 2.0;
|
||||||
|
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.3;
|
||||||
|
|
||||||
|
private ColourSwitch lastColourSwitch = ColourSwitch.None;
|
||||||
|
|
||||||
|
private int sameColourCount = 1;
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
double addition = 1;
|
||||||
|
|
||||||
|
// We get an extra addition if we are not a slider or spinner
|
||||||
|
if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)
|
||||||
|
{
|
||||||
|
if (hasColourChange(current))
|
||||||
|
addition += 0.75;
|
||||||
|
|
||||||
|
if (hasRhythmChange(current))
|
||||||
|
addition += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lastColourSwitch = ColourSwitch.None;
|
||||||
|
sameColourCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double additionFactor = 1;
|
||||||
|
|
||||||
|
// Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50
|
||||||
|
if (current.DeltaTime < 50)
|
||||||
|
additionFactor = 0.4 + 0.6 * current.DeltaTime / 50;
|
||||||
|
|
||||||
|
return additionFactor * addition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool hasRhythmChange(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
// We don't want a division by zero if some random mapper decides to put two HitObjects at the same time.
|
||||||
|
if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime);
|
||||||
|
|
||||||
|
if (timeElapsedRatio >= 8)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
|
||||||
|
|
||||||
|
return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool hasColourChange(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
var taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||||
|
|
||||||
|
if (!taikoCurrent.HasTypeChange)
|
||||||
|
{
|
||||||
|
sameColourCount++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldColourSwitch = lastColourSwitch;
|
||||||
|
var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd;
|
||||||
|
|
||||||
|
lastColourSwitch = newColourSwitch;
|
||||||
|
sameColourCount = 1;
|
||||||
|
|
||||||
|
// We only want a bonus if the parity of the color switch changes
|
||||||
|
return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ColourSwitch
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Even,
|
||||||
|
Odd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.Taiko.Difficulty
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
{
|
{
|
||||||
@ -10,10 +9,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
public double GreatHitWindow;
|
public double GreatHitWindow;
|
||||||
public int MaxCombo;
|
public int MaxCombo;
|
||||||
|
|
||||||
public TaikoDifficultyAttributes(Mod[] mods, double starRating)
|
|
||||||
: base(mods, starRating)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,137 +1,51 @@
|
|||||||
// 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.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Taiko.Mods;
|
using osu.Game.Rulesets.Taiko.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
{
|
{
|
||||||
internal class TaikoDifficultyCalculator : DifficultyCalculator
|
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double star_scaling_factor = 0.04125;
|
private const double star_scaling_factor = 0.04125;
|
||||||
|
|
||||||
/// <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;
|
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
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 TaikoDifficultyAttributes(mods, 0);
|
return new TaikoDifficultyAttributes { Mods = mods };
|
||||||
|
|
||||||
var difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
|
return new TaikoDifficultyAttributes
|
||||||
|
|
||||||
foreach (var hitObject in beatmap.HitObjects)
|
|
||||||
difficultyHitObjects.Add(new TaikoHitObjectDifficulty((TaikoHitObject)hitObject));
|
|
||||||
|
|
||||||
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
|
|
||||||
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
|
|
||||||
|
|
||||||
if (!calculateStrainValues(difficultyHitObjects, timeRate))
|
|
||||||
return new TaikoDifficultyAttributes(mods, 0);
|
|
||||||
|
|
||||||
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor;
|
|
||||||
|
|
||||||
return new TaikoDifficultyAttributes(mods, starRating)
|
|
||||||
{
|
{
|
||||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
|
StarRating = skills.Single().DifficultyValue() * star_scaling_factor,
|
||||||
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate,
|
Mods = mods,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit)
|
// 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,
|
||||||
|
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool calculateStrainValues(List<TaikoHitObjectDifficulty> objects, double timeRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
|
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||||
using (var hitObjectsEnumerator = objects.GetEnumerator())
|
yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
|
||||||
{
|
|
||||||
if (!hitObjectsEnumerator.MoveNext()) return false;
|
|
||||||
|
|
||||||
TaikoHitObjectDifficulty 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;
|
|
||||||
next?.CalculateStrains(current, timeRate);
|
|
||||||
current = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double calculateDifficulty(List<TaikoHitObjectDifficulty> objects, double timeRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() };
|
||||||
{
|
|
||||||
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
|
|
||||||
|
|
||||||
TaikoHitObjectDifficulty 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 decay = Math.Pow(TaikoHitObjectDifficulty.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
{
|
{
|
||||||
|
@ -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.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -48,9 +49,9 @@ namespace osu.Game.Rulesets.Taiko.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";
|
||||||
|
@ -1,127 +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;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects
|
|
||||||
{
|
|
||||||
internal class TaikoHitObjectDifficulty
|
|
||||||
{
|
|
||||||
/// <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 DECAY_BASE = 0.30;
|
|
||||||
|
|
||||||
private const double type_change_bonus = 0.75;
|
|
||||||
private const double rhythm_change_bonus = 1.0;
|
|
||||||
private const double rhythm_change_base_threshold = 0.2;
|
|
||||||
private const double rhythm_change_base = 2.0;
|
|
||||||
|
|
||||||
internal TaikoHitObject BaseHitObject;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Measures note density in a way
|
|
||||||
/// </summary>
|
|
||||||
internal double Strain = 1;
|
|
||||||
|
|
||||||
private double timeElapsed;
|
|
||||||
private int sameTypeSince = 1;
|
|
||||||
|
|
||||||
private bool isRim => BaseHitObject is RimHit;
|
|
||||||
|
|
||||||
public TaikoHitObjectDifficulty(TaikoHitObject baseHitObject)
|
|
||||||
{
|
|
||||||
BaseHitObject = baseHitObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CalculateStrains(TaikoHitObjectDifficulty 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.
|
|
||||||
timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
|
|
||||||
double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000);
|
|
||||||
|
|
||||||
double addition = 1;
|
|
||||||
|
|
||||||
// Only if we are no slider or spinner we get an extra addition
|
|
||||||
if (previousHitObject.BaseHitObject is Hit && BaseHitObject is Hit
|
|
||||||
&& BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime < 1000) // And we only want to check out hitobjects which aren't so far in the past
|
|
||||||
{
|
|
||||||
addition += typeChangeAddition(previousHitObject);
|
|
||||||
addition += rhythmChangeAddition(previousHitObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
double additionFactor = 1.0;
|
|
||||||
// Scale AdditionFactor linearly from 0.4 to 1 for TimeElapsed from 0 to 50
|
|
||||||
if (timeElapsed < 50.0)
|
|
||||||
additionFactor = 0.4 + 0.6 * timeElapsed / 50.0;
|
|
||||||
|
|
||||||
Strain = previousHitObject.Strain * decay + addition * additionFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TypeSwitch lastTypeSwitchEven = TypeSwitch.None;
|
|
||||||
private double typeChangeAddition(TaikoHitObjectDifficulty previousHitObject)
|
|
||||||
{
|
|
||||||
// If we don't have the same hit type, trigger a type change!
|
|
||||||
if (previousHitObject.isRim != isRim)
|
|
||||||
{
|
|
||||||
lastTypeSwitchEven = previousHitObject.sameTypeSince % 2 == 0 ? TypeSwitch.Even : TypeSwitch.Odd;
|
|
||||||
|
|
||||||
// We only want a bonus if the parity of the type switch changes!
|
|
||||||
switch (previousHitObject.lastTypeSwitchEven)
|
|
||||||
{
|
|
||||||
case TypeSwitch.Even:
|
|
||||||
if (lastTypeSwitchEven == TypeSwitch.Odd)
|
|
||||||
return type_change_bonus;
|
|
||||||
break;
|
|
||||||
case TypeSwitch.Odd:
|
|
||||||
if (lastTypeSwitchEven == TypeSwitch.Even)
|
|
||||||
return type_change_bonus;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No type change? Increment counter and keep track of last type switch
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lastTypeSwitchEven = previousHitObject.lastTypeSwitchEven;
|
|
||||||
sameTypeSince = previousHitObject.sameTypeSince + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double rhythmChangeAddition(TaikoHitObjectDifficulty previousHitObject)
|
|
||||||
{
|
|
||||||
// We don't want a division by zero if some random mapper decides to put 2 HitObjects at the same time.
|
|
||||||
if (timeElapsed == 0 || previousHitObject.timeElapsed == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double timeElapsedRatio = Math.Max(previousHitObject.timeElapsed / timeElapsed, timeElapsed / previousHitObject.timeElapsed);
|
|
||||||
|
|
||||||
if (timeElapsedRatio >= 8)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
|
|
||||||
|
|
||||||
if (isWithinChangeThreshold(difference))
|
|
||||||
return rhythm_change_bonus;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool isWithinChangeThreshold(double value)
|
|
||||||
{
|
|
||||||
return value > rhythm_change_base_threshold && value < 1 - rhythm_change_base_threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum TypeSwitch
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Even,
|
|
||||||
Odd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -225,7 +225,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||||
{
|
{
|
||||||
if (!DisplayJudgements)
|
if (!DisplayJudgements.Value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!judgedObject.DisplayResult)
|
if (!judgedObject.DisplayResult)
|
||||||
|
@ -10,4 +10,4 @@
|
|||||||
<ItemGroup Label="Project References">
|
<ItemGroup Label="Project References">
|
||||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
// 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 NUnit.Framework;
|
using NUnit.Framework;
|
||||||
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;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual
|
namespace osu.Game.Tests.NonVisual
|
||||||
@ -15,7 +18,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestNoMods()
|
public void TestNoMods()
|
||||||
{
|
{
|
||||||
var combinations = new TestDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(1, combinations.Length);
|
Assert.AreEqual(1, combinations.Length);
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
@ -24,7 +27,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSingleMod()
|
public void TestSingleMod()
|
||||||
{
|
{
|
||||||
var combinations = new TestDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(2, combinations.Length);
|
Assert.AreEqual(2, combinations.Length);
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
@ -34,7 +37,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDoubleMod()
|
public void TestDoubleMod()
|
||||||
{
|
{
|
||||||
var combinations = new TestDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(4, combinations.Length);
|
Assert.AreEqual(4, combinations.Length);
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
@ -49,7 +52,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestIncompatibleMods()
|
public void TestIncompatibleMods()
|
||||||
{
|
{
|
||||||
var combinations = new TestDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(3, combinations.Length);
|
Assert.AreEqual(3, combinations.Length);
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
@ -60,7 +63,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDoubleIncompatibleMods()
|
public void TestDoubleIncompatibleMods()
|
||||||
{
|
{
|
||||||
var combinations = new TestDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(8, combinations.Length);
|
Assert.AreEqual(8, combinations.Length);
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
@ -83,7 +86,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestIncompatibleThroughBaseType()
|
public void TestIncompatibleThroughBaseType()
|
||||||
{
|
{
|
||||||
var combinations = new TestDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(3, combinations.Length);
|
Assert.AreEqual(3, combinations.Length);
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
@ -136,9 +139,9 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestDifficultyCalculator : DifficultyCalculator
|
private class TestLegacyDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
public TestDifficultyCalculator(params Mod[] mods)
|
public TestLegacyDifficultyCalculator(params Mod[] mods)
|
||||||
: base(null, null)
|
: base(null, null)
|
||||||
{
|
{
|
||||||
DifficultyAdjustmentMods = mods;
|
DifficultyAdjustmentMods = mods;
|
||||||
@ -146,7 +149,20 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods { get; }
|
protected override Mod[] DifficultyAdjustmentMods { get; }
|
||||||
|
|
||||||
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => throw new NotImplementedException();
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
115
osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs
Normal file
115
osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class LimitedCapacityStackTest
|
||||||
|
{
|
||||||
|
private const int capacity = 3;
|
||||||
|
|
||||||
|
private LimitedCapacityStack<int> stack;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
stack = new LimitedCapacityStack<int>(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEmptyStack()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(0, stack.Count);
|
||||||
|
|
||||||
|
Assert.Throws<IndexOutOfRangeException>(() =>
|
||||||
|
{
|
||||||
|
int unused = stack[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (var unused in stack)
|
||||||
|
count++;
|
||||||
|
|
||||||
|
Assert.AreEqual(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
[TestCase(3)]
|
||||||
|
public void TestInRangeElements(int count)
|
||||||
|
{
|
||||||
|
// e.g. 0 -> 1 -> 2
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
stack.Push(i);
|
||||||
|
|
||||||
|
Assert.AreEqual(count, stack.Count);
|
||||||
|
|
||||||
|
// e.g. 2 -> 1 -> 0 (reverse order)
|
||||||
|
for (int i = 0; i < stack.Count; i++)
|
||||||
|
Assert.AreEqual(count - 1 - i, stack[i]);
|
||||||
|
|
||||||
|
// e.g. indices 3, 4, 5, 6 (out of range)
|
||||||
|
for (int i = stack.Count; i < stack.Count + capacity; i++)
|
||||||
|
{
|
||||||
|
Assert.Throws<IndexOutOfRangeException>(() =>
|
||||||
|
{
|
||||||
|
int unused = stack[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(4)]
|
||||||
|
[TestCase(5)]
|
||||||
|
[TestCase(6)]
|
||||||
|
public void TestOverflowElements(int count)
|
||||||
|
{
|
||||||
|
// e.g. 0 -> 1 -> 2 -> 3
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
stack.Push(i);
|
||||||
|
|
||||||
|
Assert.AreEqual(capacity, stack.Count);
|
||||||
|
|
||||||
|
// e.g. 3 -> 2 -> 1 (reverse order)
|
||||||
|
for (int i = 0; i < stack.Count; i++)
|
||||||
|
Assert.AreEqual(count - 1 - i, stack[i]);
|
||||||
|
|
||||||
|
// e.g. indices 3, 4, 5, 6 (out of range)
|
||||||
|
for (int i = stack.Count; i < stack.Count + capacity; i++)
|
||||||
|
{
|
||||||
|
Assert.Throws<IndexOutOfRangeException>(() =>
|
||||||
|
{
|
||||||
|
int unused = stack[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
[TestCase(3)]
|
||||||
|
[TestCase(4)]
|
||||||
|
[TestCase(5)]
|
||||||
|
[TestCase(6)]
|
||||||
|
public void TestEnumerator(int count)
|
||||||
|
{
|
||||||
|
// e.g. 0 -> 1 -> 2 -> 3
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
stack.Push(i);
|
||||||
|
|
||||||
|
int enumeratorCount = 0;
|
||||||
|
int expectedValue = count - 1;
|
||||||
|
|
||||||
|
foreach (var item in stack)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(expectedValue, item);
|
||||||
|
enumeratorCount++;
|
||||||
|
expectedValue--;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(stack.Count, enumeratorCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
protected override void AddCheckSteps(Func<Player> player)
|
protected override void AddCheckSteps(Func<Player> player)
|
||||||
{
|
{
|
||||||
base.AddCheckSteps(player);
|
base.AddCheckSteps(player);
|
||||||
AddUntilStep(() => ((ScoreAccessiblePlayer)player()).ScoreProcessor.TotalScore > 0, "score above zero");
|
AddUntilStep(() => ((ScoreAccessiblePlayer)player()).ScoreProcessor.TotalScore.Value > 0, "score above zero");
|
||||||
AddUntilStep(() => ((ScoreAccessiblePlayer)player()).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0), "key counter counted keys");
|
AddUntilStep(() => ((ScoreAccessiblePlayer)player()).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0), "key counter counted keys");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Framework.Lists;
|
using osu.Framework.Lists;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual
|
namespace osu.Game.Tests.Visual
|
||||||
{
|
{
|
||||||
@ -196,8 +197,8 @@ namespace osu.Game.Tests.Visual
|
|||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
Direction = FillDirection.Horizontal;
|
Direction = FillDirection.Horizontal;
|
||||||
Add(new OsuSpriteText { Text = header + @": ", TextSize = text_size });
|
Add(new OsuSpriteText { Text = header + @": ", Font = OsuFont.GetFont(size: text_size) });
|
||||||
Add(valueText = new OsuSpriteText { TextSize = text_size });
|
Add(valueText = new OsuSpriteText { Font = OsuFont.GetFont(size: text_size) });
|
||||||
Margin = new MarginPadding(margin);
|
Margin = new MarginPadding(margin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
private bool selectedBeatmapVisible()
|
private bool selectedBeatmapVisible()
|
||||||
{
|
{
|
||||||
var currentlySelected = carousel.Items.Find(s => s.Item is CarouselBeatmap && s.Item.State == CarouselItemState.Selected);
|
var currentlySelected = carousel.Items.Find(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected);
|
||||||
if (currentlySelected == null)
|
if (currentlySelected == null)
|
||||||
return true;
|
return true;
|
||||||
return currentlySelected.Item.Visible;
|
return currentlySelected.Item.Visible;
|
||||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("show", () =>
|
AddStep("show", () =>
|
||||||
{
|
{
|
||||||
infoWedge.State = Visibility.Visible;
|
infoWedge.State = Visibility.Visible;
|
||||||
infoWedge.Beatmap = Beatmap;
|
infoWedge.Beatmap = Beatmap.Value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// select part is redundant, but wait for load isn't
|
// select part is redundant, but wait for load isn't
|
||||||
|
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
});
|
});
|
||||||
|
|
||||||
channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
|
channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
|
||||||
channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.ToString();
|
channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.NewValue.ToString();
|
||||||
|
|
||||||
AddStep("Add random private channel", addRandomPrivateChannel);
|
AddStep("Add random private channel", addRandomPrivateChannel);
|
||||||
AddAssert("There is only one channels", () => channelTabControl.Items.Count() == 2);
|
AddAssert("There is only one channels", () => channelTabControl.Items.Count() == 2);
|
||||||
|
@ -13,10 +13,10 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Framework.Configuration;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual
|
namespace osu.Game.Tests.Visual
|
||||||
{
|
{
|
||||||
@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast<OsuSpriteText>().All(sprite => sprite.Font == "Exo2.0-MediumItalic");
|
bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast<OsuSpriteText>().All(sprite => sprite.Font.Italics);
|
||||||
|
|
||||||
bool isShowingLinks()
|
bool isShowingLinks()
|
||||||
{
|
{
|
||||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
drawableDate.Current.ValueChanged += v => flash.FadeOutFromOne(500);
|
drawableDate.Current.ValueChanged += _ => flash.FadeOutFromOne(500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, Clock);
|
||||||
Child = new ComposeScreen();
|
Child = new ComposeScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Configuration;
|
using osu.Framework.Bindables;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Beatmap.Value = new TestWorkingBeatmap(testBeatmap);
|
Beatmap.Value = new TestWorkingBeatmap(testBeatmap, Clock);
|
||||||
|
|
||||||
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
|
Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null);
|
||||||
|
|
||||||
Add(new SummaryTimeline
|
Add(new SummaryTimeline
|
||||||
{
|
{
|
||||||
|
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First()));
|
AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First()));
|
||||||
AddStep("Hide overlay", () => failOverlay.Hide());
|
AddStep("Hide overlay", () => failOverlay.Hide());
|
||||||
|
|
||||||
AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected));
|
AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void press(Key key)
|
private void press(Key key)
|
||||||
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("Show overlay", () => pauseOverlay.Show());
|
AddStep("Show overlay", () => pauseOverlay.Show());
|
||||||
|
|
||||||
AddStep("Up arrow", () => press(Key.Up));
|
AddStep("Up arrow", () => press(Key.Up));
|
||||||
AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected);
|
AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value);
|
||||||
|
|
||||||
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
||||||
}
|
}
|
||||||
@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("Show overlay", () => pauseOverlay.Show());
|
AddStep("Show overlay", () => pauseOverlay.Show());
|
||||||
|
|
||||||
AddStep("Down arrow", () => press(Key.Down));
|
AddStep("Down arrow", () => press(Key.Down));
|
||||||
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected);
|
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected.Value);
|
||||||
|
|
||||||
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
||||||
}
|
}
|
||||||
@ -132,11 +132,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("Show overlay", () => failOverlay.Show());
|
AddStep("Show overlay", () => failOverlay.Show());
|
||||||
|
|
||||||
AddStep("Up arrow", () => press(Key.Up));
|
AddStep("Up arrow", () => press(Key.Up));
|
||||||
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected);
|
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
|
||||||
AddStep("Up arrow", () => press(Key.Up));
|
AddStep("Up arrow", () => press(Key.Up));
|
||||||
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected);
|
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
|
||||||
AddStep("Up arrow", () => press(Key.Up));
|
AddStep("Up arrow", () => press(Key.Up));
|
||||||
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected);
|
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
|
||||||
|
|
||||||
AddStep("Hide overlay", () => failOverlay.Hide());
|
AddStep("Hide overlay", () => failOverlay.Hide());
|
||||||
}
|
}
|
||||||
@ -149,11 +149,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("Show overlay", () => failOverlay.Show());
|
AddStep("Show overlay", () => failOverlay.Show());
|
||||||
|
|
||||||
AddStep("Down arrow", () => press(Key.Down));
|
AddStep("Down arrow", () => press(Key.Down));
|
||||||
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected);
|
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
|
||||||
AddStep("Down arrow", () => press(Key.Down));
|
AddStep("Down arrow", () => press(Key.Down));
|
||||||
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected);
|
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
|
||||||
AddStep("Down arrow", () => press(Key.Down));
|
AddStep("Down arrow", () => press(Key.Down));
|
||||||
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected);
|
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
|
||||||
|
|
||||||
AddStep("Hide overlay", () => failOverlay.Hide());
|
AddStep("Hide overlay", () => failOverlay.Hide());
|
||||||
}
|
}
|
||||||
@ -169,8 +169,8 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
AddStep("Down arrow", () => press(Key.Down));
|
AddStep("Down arrow", () => press(Key.Down));
|
||||||
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
|
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
|
||||||
AddAssert("First button not selected", () => !pauseOverlay.Buttons.First().Selected);
|
AddAssert("First button not selected", () => !pauseOverlay.Buttons.First().Selected.Value);
|
||||||
AddAssert("Second button selected", () => secondButton.Selected);
|
AddAssert("Second button selected", () => secondButton.Selected.Value);
|
||||||
|
|
||||||
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
||||||
}
|
}
|
||||||
@ -190,8 +190,8 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
|
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
|
||||||
AddStep("Up arrow", () => press(Key.Up));
|
AddStep("Up arrow", () => press(Key.Up));
|
||||||
AddAssert("Second button not selected", () => !secondButton.Selected);
|
AddAssert("Second button not selected", () => !secondButton.Selected.Value);
|
||||||
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected);
|
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected.Value);
|
||||||
|
|
||||||
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
||||||
}
|
}
|
||||||
@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
|
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
|
||||||
AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero));
|
AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero));
|
||||||
AddStep("Down arrow", () => press(Key.Down));
|
AddStep("Down arrow", () => press(Key.Down));
|
||||||
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected); // Initial state condition
|
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected.Value); // Initial state condition
|
||||||
|
|
||||||
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
AddStep("Hide overlay", () => pauseOverlay.Hide());
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Text = "Fired!",
|
Text = "Fired!",
|
||||||
TextSize = 50,
|
Font = OsuFont.GetFont(size: 50),
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user