1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-20 09:20:15 +08:00

Compare commits

..

522 Commits

276 changed files with 4480 additions and 1658 deletions
+4 -10
View File
@@ -2,12 +2,6 @@
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "0.35.0",
"commands": [
"dotnet-cake"
]
},
"dotnet-format": {
"version": "3.1.37601",
"commands": [
@@ -20,20 +14,20 @@
"jb"
]
},
"nvika": {
"version": "2.0.0",
"smoogipoo.nvika": {
"version": "1.0.1",
"commands": [
"nvika"
]
},
"codefilesanity": {
"version": "15.0.0",
"version": "0.0.36",
"commands": [
"CodeFileSanity"
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.524.0",
"version": "2021.608.0",
"commands": [
"localisation"
]
+93
View File
@@ -0,0 +1,93 @@
on: [push, pull_request]
name: Continuous Integration
jobs:
test:
name: Test
runs-on: ${{matrix.os.fullname}}
env:
OSU_EXECUTION_MODE: ${{matrix.threadingMode}}
strategy:
fail-fast: false
matrix:
os:
- { prettyname: Windows, fullname: windows-latest }
- { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# FIXME: libavformat is not included in Ubuntu. Let's fix that.
# https://github.com/ppy/osu-framework/issues/4349
# Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
- name: Install libavformat-dev
if: ${{matrix.os.fullname == 'ubuntu-latest'}}
run: |
sudo apt-get update && \
sudo apt-get -y install libavformat-dev
- name: Compile
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
shell: pwsh
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
- name: Upload Test Results
uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 5.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
dotnet codefilesanity | while read -r line; do
echo "::warning::$line"
done
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
+31
View File
@@ -0,0 +1,31 @@
# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
# See:
# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
name: Annotate CI run with test results
on:
workflow_run:
workflows: ["Continuous Integration"]
types:
- completed
jobs:
annotate:
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
strategy:
fail-fast: false
matrix:
os:
- { prettyname: Windows }
- { prettyname: macOS }
- { prettyname: Linux }
threadingMode: ['SingleThread', 'MultiThreaded']
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.4.2
with:
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
-14
View File
@@ -113,20 +113,6 @@
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build benchmarks",
"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
}
]
}
+10 -26
View File
@@ -1,27 +1,11 @@
[CmdletBinding()]
Param(
[string]$Target,
[string]$Configuration,
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity,
[switch]$ShowDescription,
[Alias("WhatIf", "Noop")]
[switch]$DryRun,
[Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
[string[]]$ScriptArgs
)
# Build Cake arguments
$cakeArguments = "";
if ($Target) { $cakeArguments += "-target=$Target" }
if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
if ($ShowDescription) { $cakeArguments += "-showdescription" }
if ($DryRun) { $cakeArguments += "-dryrun" }
if ($Experimental) { $cakeArguments += "-experimental" }
$cakeArguments += $ScriptArgs
dotnet tool restore
dotnet cake ./build/InspectCode.cake --bootstrap
dotnet cake ./build/InspectCode.cake $cakeArguments
exit $LASTEXITCODE
# Temporarily disabled until the tool is upgraded to 5.0.
# The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7.
# - cmd: dotnet format --dry-run --check
dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
exit $LASTEXITCODE
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
dotnet tool restore
dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+9 -6
View File
@@ -1,24 +1,27 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2019
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: '0.0.{build}'
cache:
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
before_build:
- ps: dotnet --info # Useful when version mismatch between CI and local
- ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
- cmd: dotnet --info # Useful when version mismatch between CI and local
- cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
build:
project: osu.sln
parallel: true
verbosity: minimal
publish_nuget: true
after_build:
- ps: dotnet tool restore
- ps: dotnet format --dry-run --check
- ps: .\InspectCode.ps1
test:
assemblies:
except:
-17
View File
@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.Build.Traversal">
<ItemGroup>
<ProjectReference Include="..\osu.Desktop\osu.Desktop.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Catch.Tests\osu.Game.Rulesets.Catch.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania.Tests\osu.Game.Rulesets.Mania.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Osu.Tests\osu.Game.Rulesets.Osu.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko.Tests\osu.Game.Rulesets.Taiko.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Tournament.Tests\osu.Game.Tournament.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Tournament\osu.Game.Tournament.csproj" />
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
</Project>
-41
View File
@@ -1,41 +0,0 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.36"
///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS
///////////////////////////////////////////////////////////////////////////////
var target = Argument("target", "CodeAnalysis");
var configuration = Argument("configuration", "Release");
var rootDirectory = new DirectoryPath("..");
var sln = rootDirectory.CombineWithFilePath("osu.sln");
var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("InspectCode")
.Does(() => {
var inspectcodereport = "inspectcodereport.xml";
var cacheDir = "inspectcode";
var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output
DotNetCoreTool(rootDirectory.FullPath,
"jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}");
DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors");
});
Task("CodeFileSanity")
.Does(() => {
ValidateCodeSanity(new ValidateCodeSanitySettings {
RootDirectory = rootDirectory.FullPath,
IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
});
});
Task("CodeAnalysis")
.IsDependentOn("CodeFileSanity")
.IsDependentOn("InspectCode");
RunTarget(target);
-5
View File
@@ -1,5 +0,0 @@
[Nuget]
Source=https://api.nuget.org/v3/index.json
UseInProcessClient=true
LoadDependencies=true
+2 -2
View File
@@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.525.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.608.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.616.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,2 @@
[General]
// no version specified means v1
@@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private void addToPlayfield(DrawableCatchHitObject drawable)
{
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawable);
drawableRuleset.Playfield.Add(drawable);
}
@@ -8,9 +8,7 @@ namespace osu.Game.Rulesets.Catch
Fruit,
Banana,
Droplet,
CatcherIdle,
CatcherFail,
CatcherKiai,
Catcher,
CatchComboCounter
}
}
@@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyAttributes : DifficultyAttributes
{
public double ApproachRate;
public double ApproachRate { get; set; }
}
}
@@ -39,10 +39,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss);
misses = Score.Statistics.GetOrDefault(HitResult.Miss);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
@@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
{
public override double ScoreMultiplier => 1.12;
public override bool Ranked => true;
public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
}
@@ -33,13 +33,13 @@ namespace osu.Game.Rulesets.Catch.Mods
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
{
private readonly Catcher catcher;
private readonly CatcherArea catcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public MouseInputHelper(CatchPlayfield playfield)
{
catcher = playfield.CatcherArea.MovableCatcher;
catcherArea = playfield.CatcherArea;
RelativeSizeAxes = Axes.Both;
}
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e)
{
catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
catcherArea.SetCatcherPosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
return base.OnMouseMove(e);
}
}
@@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class DefaultCatcher : CompositeDrawable
{
public Bindable<CatcherAnimationState> CurrentState { get; } = new Bindable<CatcherAnimationState>();
private readonly Sprite sprite;
private readonly Dictionary<CatcherAnimationState, Texture> textures = new Dictionary<CatcherAnimationState, Texture>();
public DefaultCatcher()
{
RelativeSizeAxes = Axes.Both;
InternalChild = sprite = new Sprite
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit
};
}
[BackgroundDependencyLoader]
private void load(TextureStore store, Bindable<CatcherAnimationState> currentState)
{
CurrentState.BindTo(currentState);
textures[CatcherAnimationState.Idle] = store.Get(@"Gameplay/catch/fruit-catcher-idle");
textures[CatcherAnimationState.Fail] = store.Get(@"Gameplay/catch/fruit-catcher-fail");
textures[CatcherAnimationState.Kiai] = store.Get(@"Gameplay/catch/fruit-catcher-kiai");
}
protected override void LoadComplete()
{
base.LoadComplete();
CurrentState.BindValueChanged(state => sprite.Texture = textures[state.NewValue], true);
}
}
}
@@ -65,17 +65,21 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null;
case CatchSkinComponents.CatcherIdle:
return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.Catcher:
var version = Source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
case CatchSkinComponents.CatcherFail:
return this.GetAnimation("fruit-catcher-fail", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true);
if (version < 2.3m)
{
if (GetTexture(@"fruit-ryuuta") != null ||
GetTexture(@"fruit-ryuuta-0") != null)
return new LegacyCatcherOld();
}
case CatchSkinComponents.CatcherKiai:
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true);
if (GetTexture(@"fruit-catcher-idle") != null ||
GetTexture(@"fruit-catcher-idle-0") != null)
return new LegacyCatcherNew();
return null;
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
@@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public class LegacyCatcherNew : CompositeDrawable
{
[Resolved]
private Bindable<CatcherAnimationState> currentState { get; set; }
private readonly Dictionary<CatcherAnimationState, Drawable> drawables = new Dictionary<CatcherAnimationState, Drawable>();
private Drawable currentDrawable;
public LegacyCatcherNew()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
foreach (var state in Enum.GetValues(typeof(CatcherAnimationState)).Cast<CatcherAnimationState>())
{
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
{
d.Anchor = Anchor.TopCentre;
d.Origin = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One;
d.FillMode = FillMode.Fit;
d.Alpha = 0;
}));
}
currentDrawable = drawables[CatcherAnimationState.Idle];
Drawable getDrawableFor(CatcherAnimationState state) =>
skin.GetAnimation(@$"fruit-catcher-{state.ToString().ToLowerInvariant()}", true, true, true) ??
skin.GetAnimation(@"fruit-catcher-idle", true, true, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
currentState.BindValueChanged(state =>
{
currentDrawable.Alpha = 0;
currentDrawable = drawables[state.NewValue];
currentDrawable.Alpha = 1;
(currentDrawable as IFramedAnimation)?.GotoFrame(0);
}, true);
}
}
}
@@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public class LegacyCatcherOld : CompositeDrawable
{
public LegacyCatcherOld()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
InternalChild = skin.GetAnimation(@"fruit-ryuuta", true, true, true).With(d =>
{
d.Anchor = Anchor.TopCentre;
d.Origin = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One;
d.FillMode = FillMode.Fit;
});
}
}
}
+23 -128
View File
@@ -7,10 +7,8 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -25,7 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
public class Catcher : SkinReloadableDrawable, IKeyBindingHandler<CatchAction>
public class Catcher : SkinReloadableDrawable
{
/// <summary>
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
@@ -53,6 +51,11 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
public const double BASE_SPEED = 1.0;
/// <summary>
/// The current speed of the catcher.
/// </summary>
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
/// <summary>
/// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
/// </summary>
@@ -78,24 +81,23 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
private readonly Container<CaughtObject> droppedObjectTarget;
public CatcherAnimationState CurrentState { get; private set; }
public CatcherAnimationState CurrentState
{
get => body.AnimationState.Value;
private set => body.AnimationState.Value = value;
}
/// <summary>
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
/// </summary>
public const float ALLOWED_CATCH_RANGE = 0.8f;
/// <summary>
/// The drawable catcher for <see cref="CurrentState"/>.
/// </summary>
internal Drawable CurrentDrawableCatcher => currentCatcher.Drawable;
private bool dashing;
public bool Dashing
{
get => dashing;
protected set
set
{
if (value == dashing) return;
@@ -105,22 +107,22 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
public Direction VisualDirection
{
get => Scale.X > 0 ? Direction.Right : Direction.Left;
set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y);
}
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
private readonly float catchWidth;
private readonly CatcherSprite catcherIdle;
private readonly CatcherSprite catcherKiai;
private readonly CatcherSprite catcherFail;
private CatcherSprite currentCatcher;
private readonly SkinnableCatcher body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
private int currentDirection;
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
@@ -156,21 +158,7 @@ namespace osu.Game.Rulesets.Catch.UI
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
{
Anchor = Anchor.TopCentre,
Alpha = 0,
},
catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai)
{
Anchor = Anchor.TopCentre,
Alpha = 0,
},
catcherFail = new CatcherSprite(CatcherAnimationState.Fail)
{
Anchor = Anchor.TopCentre,
Alpha = 0,
},
body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
@@ -184,8 +172,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
trails = new CatcherTrailDisplay(this);
updateCatcher();
}
protected override void LoadComplete()
@@ -273,17 +259,16 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState();
if (result.IsHit)
updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle);
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
else if (!(hitObject is Banana))
updateState(CatcherAnimationState.Fail);
CurrentState = CatcherAnimationState.Fail;
}
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
var catchResult = (CatchJudgementResult)result;
if (CurrentState != catchResult.CatcherAnimationState)
updateState(catchResult.CatcherAnimationState);
CurrentState = catchResult.CatcherAnimationState;
if (HyperDashing != catchResult.CatcherHyperDash)
{
@@ -328,55 +313,6 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
public void UpdatePosition(float position)
{
position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
if (position == X)
return;
Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
X = position;
}
public bool OnPressed(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection--;
return true;
case CatchAction.MoveRight:
currentDirection++;
return true;
case CatchAction.Dash:
Dashing = true;
return true;
}
return false;
}
public void OnReleased(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection++;
break;
case CatchAction.MoveRight:
currentDirection--;
break;
case CatchAction.Dash:
Dashing = false;
break;
}
}
/// <summary>
/// Drop any fruit off the plate.
/// </summary>
@@ -418,15 +354,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
if (currentDirection == 0) return;
var direction = Math.Sign(currentDirection);
var dashModifier = Dashing ? 1 : 0.5;
var speed = BASE_SPEED * dashModifier * hyperDashModifier;
UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X))
@@ -436,38 +363,6 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
private void updateCatcher()
{
currentCatcher?.Hide();
switch (CurrentState)
{
default:
currentCatcher = catcherIdle;
break;
case CatcherAnimationState.Fail:
currentCatcher = catcherFail;
break;
case CatcherAnimationState.Kiai:
currentCatcher = catcherKiai;
break;
}
currentCatcher.Show();
(currentCatcher.Drawable as IFramedAnimation)?.GotoFrame(0);
}
private void updateState(CatcherAnimationState state)
{
if (CurrentState == state)
return;
CurrentState = state;
updateCatcher();
}
private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position)
{
var caughtObject = getCaughtObject(drawableObject.HitObject);
+72 -6
View File
@@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -14,13 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherArea : Container
public class CatcherArea : Container, IKeyBindingHandler<CatchAction>
{
public const float CATCHER_SIZE = 106.75f;
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
/// <summary>
/// <c>-1</c> when only left button is pressed.
/// <c>1</c> when only right button is pressed.
/// <c>0</c> when none or both left and right buttons are pressed.
/// </summary>
private int currentDirection;
public CatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
@@ -63,16 +72,73 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.OnRevertResult(hitObject, result);
}
protected override void Update()
{
base.Update();
var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
SetCatcherPosition(
replayState?.CatcherX ??
(float)(MovableCatcher.X + MovableCatcher.Speed * currentDirection * Clock.ElapsedFrameTime));
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
var state = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState<CatchAction>)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
comboDisplay.X = MovableCatcher.X;
}
public void SetCatcherPosition(float X)
{
float lastPosition = MovableCatcher.X;
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
MovableCatcher.X = newPosition;
if (lastPosition < newPosition)
MovableCatcher.VisualDirection = Direction.Right;
else if (lastPosition > newPosition)
MovableCatcher.VisualDirection = Direction.Left;
}
public bool OnPressed(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection--;
return true;
case CatchAction.MoveRight:
currentDirection++;
return true;
case CatchAction.Dash:
MovableCatcher.Dashing = true;
return true;
}
return false;
}
public void OnReleased(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection++;
break;
case CatchAction.MoveRight:
currentDirection--;
break;
case CatchAction.Dash:
MovableCatcher.Dashing = false;
break;
}
}
}
}
@@ -1,59 +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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherSprite : SkinnableDrawable
{
protected override bool ApplySizeRestrictionsToDefault => true;
public CatcherSprite(CatcherAnimationState state)
: base(new CatchSkinComponent(componentFromState(state)), _ =>
new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleToFit)
{
RelativeSizeAxes = Axes.None;
Size = new Vector2(CatcherArea.CATCHER_SIZE);
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
private static CatchSkinComponents componentFromState(CatcherAnimationState state)
{
switch (state)
{
case CatcherAnimationState.Fail:
return CatchSkinComponents.CatcherFail;
case CatcherAnimationState.Kiai:
return CatchSkinComponents.CatcherKiai;
default:
return CatchSkinComponents.CatcherIdle;
}
}
private class DefaultCatcherSprite : Sprite
{
private readonly CatcherAnimationState state;
public DefaultCatcherSprite(CatcherAnimationState state)
{
this.state = state;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = textures.Get($"Gameplay/catch/fruit-catcher-{state.ToString().ToLower()}");
}
}
}
}
@@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Timing;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// A trail of the catcher.
/// It also represents a hyper dash afterimage.
/// </summary>
public class CatcherTrail : PoolableDrawable
{
public CatcherAnimationState AnimationState
{
set => body.AnimationState.Value = value;
}
private readonly SkinnableCatcher body;
public CatcherTrail()
{
Size = new Vector2(CatcherArea.CATCHER_SIZE);
Origin = Anchor.TopCentre;
Blending = BlendingParameters.Additive;
InternalChild = body = new SkinnableCatcher
{
// Using a frozen clock because trails should not be animated when the skin has an animated catcher.
// TODO: The animation should be frozen at the animation frame at the time of the trail generation.
Clock = new FramedClock(new ManualClock()),
};
}
protected override void FreeAfterUse()
{
ClearTransforms();
base.FreeAfterUse();
}
}
}
@@ -4,10 +4,8 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
@@ -21,11 +19,11 @@ namespace osu.Game.Rulesets.Catch.UI
{
private readonly Catcher catcher;
private readonly DrawablePool<CatcherTrailSprite> trailPool;
private readonly DrawablePool<CatcherTrail> trailPool;
private readonly Container<CatcherTrailSprite> dashTrails;
private readonly Container<CatcherTrailSprite> hyperDashTrails;
private readonly Container<CatcherTrailSprite> endGlowSprites;
private readonly Container<CatcherTrail> dashTrails;
private readonly Container<CatcherTrail> hyperDashTrails;
private readonly Container<CatcherTrail> endGlowSprites;
private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
@@ -85,10 +83,10 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[]
{
trailPool = new DrawablePool<CatcherTrailSprite>(30),
dashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both },
hyperDashTrails = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
endGlowSprites = new Container<CatcherTrailSprite> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
trailPool = new DrawablePool<CatcherTrail>(30),
dashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both },
hyperDashTrails = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
endGlowSprites = new Container<CatcherTrail> { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR },
};
}
@@ -118,17 +116,12 @@ namespace osu.Game.Rulesets.Catch.UI
Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50);
}
private CatcherTrailSprite createTrailSprite(Container<CatcherTrailSprite> target)
private CatcherTrail createTrailSprite(Container<CatcherTrail> target)
{
var texture = (catcher.CurrentDrawableCatcher as TextureAnimation)?.CurrentFrame ?? ((Sprite)catcher.CurrentDrawableCatcher).Texture;
CatcherTrail sprite = trailPool.Get();
CatcherTrailSprite sprite = trailPool.Get();
sprite.Texture = texture;
sprite.Anchor = catcher.Anchor;
sprite.AnimationState = catcher.CurrentState;
sprite.Scale = catcher.Scale;
sprite.Blending = BlendingParameters.Additive;
sprite.RelativePositionAxes = catcher.RelativePositionAxes;
sprite.Position = catcher.Position;
target.Add(sprite);
@@ -1,40 +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.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherTrailSprite : PoolableDrawable
{
public Texture Texture
{
set => sprite.Texture = value;
}
private readonly Sprite sprite;
public CatcherTrailSprite()
{
InternalChild = sprite = new Sprite
{
RelativeSizeAxes = Axes.Both
};
Size = new Vector2(CatcherArea.CATCHER_SIZE);
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
protected override void FreeAfterUse()
{
ClearTransforms();
base.FreeAfterUse();
}
}
}
@@ -1,11 +1,11 @@
// 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.
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
namespace osu.Game.Rulesets.Catch.UI
{
public enum HoldNotePosition
public enum Direction
{
Start,
End
Right = 1,
Left = -1
}
}
@@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// The visual representation of the <see cref="Catcher"/>.
/// It includes the body part of the catcher and the catcher plate.
/// </summary>
public class SkinnableCatcher : SkinnableDrawable
{
/// <summary>
/// This is used by skin elements to determine which texture of the catcher is used.
/// </summary>
[Cached]
public readonly Bindable<CatcherAnimationState> AnimationState = new Bindable<CatcherAnimationState>();
public SkinnableCatcher()
: base(new CatchSkinComponent(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
{
Anchor = Anchor.TopCentre;
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
}
}
@@ -1,31 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
[Cached(Type = typeof(IAdjustableClock))]
private readonly IAdjustableClock clock = new StopwatchClock();
protected override Container<Drawable> Content => blueprints ?? base.Content;
protected ManiaSelectionBlueprintTestScene()
private readonly Container blueprints;
[Cached(typeof(Playfield))]
public Playfield Playfield { get; }
private readonly ScrollingTestContainer scrollingTestContainer;
protected ScrollingDirection Direction
{
Add(new Column(0)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
set => scrollingTestContainer.Direction = value;
}
public ManiaPlayfield Playfield => null;
protected ManiaSelectionBlueprintTestScene(int columns)
{
var stageDefinitions = new List<StageDefinition> { new StageDefinition { Columns = columns } };
base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Playfield = new ManiaPlayfield(stageDefinitions)
{
RelativeSizeAxes = Axes.Both,
},
blueprints = new Container
{
RelativeSizeAxes = Axes.Both
}
}
};
AddToggleStep("Downward scroll", b => Direction = b ? ScrollingDirection.Down : ScrollingDirection.Up);
}
}
}
@@ -1,55 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
private readonly DrawableHoldNote drawableObject;
protected override Container<Drawable> Content => content ?? base.Content;
private readonly Container content;
public TestSceneHoldNoteSelectionBlueprint()
: base(4)
{
var holdNote = new HoldNote { Column = 0, Duration = 1000 };
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
for (int i = 0; i < 4; i++)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Width = 50,
Child = drawableObject = new DrawableHoldNote(holdNote)
var holdNote = new HoldNote
{
Height = 300,
AccentColour = { Value = OsuColour.Gray(0.3f) }
}
};
Column = i,
StartTime = i * 100,
Duration = 500
};
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableObject);
}
protected override void Update()
{
base.Update();
foreach (var nested in drawableObject.NestedHitObjects)
{
double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration;
nested.Y = (float)(-finalPosition * content.DrawHeight);
var drawableHitObject = new DrawableHoldNote(holdNote);
Playfield.Add(drawableHitObject);
AddBlueprint(new HoldNoteSelectionBlueprint(holdNote), drawableHitObject);
}
}
}
@@ -12,7 +12,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning.Default;
@@ -184,8 +184,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteOverlay>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteOverlay>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<EditNotePiece>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
}
private void setScrollStep(ScrollingDirection direction)
@@ -1,40 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{
protected override Container<Drawable> Content => content ?? base.Content;
private readonly Container content;
public TestSceneNoteSelectionBlueprint()
: base(4)
{
var note = new Note { Column = 0 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
DrawableNote drawableObject;
base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
for (int i = 0; i < 4; i++)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(50, 20),
Child = drawableObject = new DrawableNote(note)
};
var note = new Note
{
Column = i,
StartTime = i * 200,
};
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
AddBlueprint(new NoteSelectionBlueprint(note), drawableObject);
var drawableHitObject = new DrawableNote(note);
Playfield.Add(drawableHitObject);
AddBlueprint(new NoteSelectionBlueprint(note), drawableHitObject);
}
}
}
}
@@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
public double ScoreMultiplier;
public double GreatHitWindow { get; set; }
public double ScoreMultiplier { get; set; }
}
}
@@ -44,9 +44,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
if (mods.Any(m => !m.Ranked))
return 0;
IEnumerable<Mod> scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
double scoreMultiplier = 1.0;
@@ -1,43 +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.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class HoldNoteNoteOverlay : CompositeDrawable
{
private readonly HoldNoteSelectionBlueprint holdNoteBlueprint;
private readonly HoldNotePosition position;
public HoldNoteNoteOverlay(HoldNoteSelectionBlueprint holdNoteBlueprint, HoldNotePosition position)
{
this.holdNoteBlueprint = holdNoteBlueprint;
this.position = position;
InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
}
protected override void Update()
{
base.Update();
var drawableObject = holdNoteBlueprint.DrawableObject;
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
if (drawableObject.IsLoaded)
{
DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)drawableObject.Head : drawableObject.Tail;
Anchor = note.Anchor;
Origin = note.Origin;
Size = note.DrawSize;
Position = note.DrawPosition;
}
}
}
}
@@ -2,14 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -17,13 +16,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint<HoldNote>
{
public new DrawableHoldNote DrawableObject => (DrawableHoldNote)base.DrawableObject;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
[Resolved]
private OsuColour colours { get; set; }
private EditNotePiece head;
private EditNotePiece tail;
public HoldNoteSelectionBlueprint(HoldNote hold)
: base(hold)
{
@@ -32,12 +30,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
InternalChildren = new Drawable[]
{
new HoldNoteNoteOverlay(this, HoldNotePosition.Start),
new HoldNoteNoteOverlay(this, HoldNotePosition.End),
head = new EditNotePiece { RelativeSizeAxes = Axes.X },
tail = new EditNotePiece { RelativeSizeAxes = Axes.X },
new Container
{
RelativeSizeAxes = Axes.Both,
@@ -58,21 +54,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
if (DrawableObject.IsLoaded)
{
Size = DrawableObject.DrawSize + new Vector2(0, DrawableObject.Tail.DrawHeight);
// This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do
// When scrolling upwards our origin is already at the top of the head note (which is the intended location),
// but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note)
if (direction.Value == ScrollingDirection.Down)
Y -= DrawableObject.Tail.DrawHeight;
}
head.Y = HitObjectContainer.PositionAtTime(HitObject.Head.StartTime, HitObject.StartTime);
tail.Y = HitObjectContainer.PositionAtTime(HitObject.Tail.StartTime, HitObject.StartTime);
Height = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime) + tail.DrawHeight;
}
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
public override Vector2 ScreenSpaceSelectionPoint => head.ScreenSpaceDrawQuad.Centre;
}
}
@@ -5,20 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public abstract class ManiaSelectionBlueprint<T> : HitObjectSelectionBlueprint<T>
where T : ManiaHitObject
{
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
[Resolved]
private Playfield playfield { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
protected ManiaSelectionBlueprint(T hitObject)
: base(hitObject)
{
@@ -29,19 +32,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.Update();
Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
}
var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
Anchor = Origin = anchor;
foreach (var child in InternalChildren)
child.Anchor = child.Origin = anchor;
public override void Show()
{
DrawableObject.AlwaysAlive = true;
base.Show();
}
public override void Hide()
{
DrawableObject.AlwaysAlive = false;
base.Hide();
Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
Width = HitObjectContainer.DrawWidth;
}
}
}
@@ -14,14 +14,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
}
protected override void Update()
{
base.Update();
// Todo: This shouldn't exist, mania should not reference the drawable hitobject directly.
if (DrawableObject.IsLoaded)
Size = DrawableObject.DrawSize;
}
}
}
@@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
public override bool Ranked => true;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
{
@@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
public override bool Ranked => false;
public void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
{
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
@@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
public override string Description => "Notes are flipped horizontally.";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
public void ApplyToBeatmap(IBeatmap beatmap)
{
@@ -85,63 +85,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AccentColour.UnbindFrom(ParentHitObject.AccentColour);
}
private double computedLifetimeStart;
public override double LifetimeStart
{
get => base.LifetimeStart;
set
{
computedLifetimeStart = value;
if (!AlwaysAlive)
base.LifetimeStart = value;
}
}
private double computedLifetimeEnd;
public override double LifetimeEnd
{
get => base.LifetimeEnd;
set
{
computedLifetimeEnd = value;
if (!AlwaysAlive)
base.LifetimeEnd = value;
}
}
private bool alwaysAlive;
/// <summary>
/// Whether this <see cref="DrawableManiaHitObject"/> should always remain alive.
/// </summary>
internal bool AlwaysAlive
{
get => alwaysAlive;
set
{
if (alwaysAlive == value)
return;
alwaysAlive = value;
if (value)
{
// Set the base lifetimes directly, to avoid mangling the computed lifetimes
base.LifetimeStart = double.MinValue;
base.LifetimeEnd = double.MaxValue;
}
else
{
LifetimeStart = computedLifetimeStart;
LifetimeEnd = computedLifetimeEnd;
}
}
}
protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
isLegacySkin = new Lazy<bool>(() => FindProvider(s => s.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
hasKeyTexture = new Lazy<bool>(() => FindProvider(s => s.GetAnimation(
this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
s.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
?? "mania-key1", true, true) != null) != null);
}
@@ -0,0 +1,114 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor
{
[Resolved]
private OsuConfigManager config { get; set; }
[Test]
public void TestHitCircleAnimationDisable()
{
HitCircle hitCircle = null;
DrawableHitCircle drawableHitCircle = null;
AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0));
toggleAnimations(true);
seekSmoothlyTo(() => hitCircle.StartTime + 10);
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
assertFutureTransforms(() => drawableHitCircle.CirclePiece, true);
AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1));
toggleAnimations(false);
seekSmoothlyTo(() => hitCircle.StartTime + 10);
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
assertFutureTransforms(() => drawableHitCircle.CirclePiece, false);
AddAssert("hit circle has longer fade-out applied", () =>
{
var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha));
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
});
}
[Test]
public void TestSliderAnimationDisable()
{
Slider slider = null;
DrawableSlider drawableSlider = null;
DrawableSliderRepeat sliderRepeat = null;
AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0));
toggleAnimations(true);
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
retrieveDrawables();
assertFutureTransforms(() => sliderRepeat, true);
AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1));
toggleAnimations(false);
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
retrieveDrawables();
assertFutureTransforms(() => sliderRepeat.Arrow, false);
seekSmoothlyTo(() => slider.GetEndTime());
AddAssert("slider has longer fade-out applied", () =>
{
var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha));
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
});
void retrieveDrawables() =>
AddStep("retrieve drawables", () =>
{
drawableSlider = (DrawableSlider)getDrawableObjectFor(slider);
sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType<SliderRepeat>().First());
});
}
private HitCircle getHitCircle(int index)
=> EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(index);
private Slider getSliderWithRepeats(int index)
=> EditorBeatmap.HitObjects.OfType<Slider>().Where(s => s.RepeatCount >= 1).ElementAt(index);
private DrawableHitObject getDrawableObjectFor(HitObject hitObject)
=> this.ChildrenOfType<DrawableHitObject>().Single(ho => ho.HitObject == hitObject);
private IEnumerable<Transform> getTransformsRecursively(Drawable drawable)
=> drawable.ChildrenOfType<Drawable>().SelectMany(d => d.Transforms);
private void toggleAnimations(bool enabled)
=> AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled));
private void seekSmoothlyTo(Func<double> targetTime)
{
AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke()));
AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime));
}
private void assertFutureTransforms(Func<Drawable> getDrawable, bool hasFutureTransforms)
=> AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms",
() => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms);
}
}
@@ -1,6 +1,6 @@
[General]
Version: 1.0
// no version specified means v1
[Fonts]
HitCircleOverlap: 3
ScoreOverlap: 3
ScoreOverlap: 3
@@ -21,20 +21,37 @@ namespace osu.Game.Rulesets.Osu.Tests
private int depthIndex;
[Test]
public void TestVariousHitCircles()
public void TestHits()
{
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
}
[Test]
public void TestHittingEarly()
{
AddStep("Hit stream early", () => SetContents(_ => testStream(5, true, -150)));
}
[Test]
public void TestMisses()
{
AddStep("Miss Big Single", () => SetContents(_ => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(_ => testSingle(5)));
AddStep("Miss Small Single", () => SetContents(_ => testSingle(7)));
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
AddStep("Hit Medium Single", () => SetContents(_ => testSingle(5, true)));
AddStep("Hit Small Single", () => SetContents(_ => testSingle(7, true)));
AddStep("Miss Big Stream", () => SetContents(_ => testStream(2)));
AddStep("Miss Medium Stream", () => SetContents(_ => testStream(5)));
AddStep("Miss Small Stream", () => SetContents(_ => testStream(7)));
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
}
[Test]
public void TestHittingLate()
{
AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150)));
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
@@ -46,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests
return playfield;
}
private Drawable testStream(float circleSize, bool auto = false)
private Drawable testStream(float circleSize, bool auto = false, double hitOffset = 0)
{
var playfield = new TestOsuPlayfield();
@@ -54,14 +71,14 @@ namespace osu.Game.Rulesets.Osu.Tests
for (int i = 0; i <= 1000; i += 100)
{
playfield.Add(createSingle(circleSize, auto, i, pos));
playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset));
pos.X += 50;
}
return playfield;
}
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset)
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0)
{
positionOffset ??= Vector2.Zero;
@@ -73,14 +90,14 @@ namespace osu.Game.Rulesets.Osu.Tests
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
var drawable = CreateDrawableHitCircle(circle, auto);
var drawable = CreateDrawableHitCircle(circle, auto, hitOffset);
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawable);
return drawable;
}
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0) => new TestDrawableHitCircle(circle, auto, hitOffset)
{
Depth = depthIndex++
};
@@ -88,18 +105,20 @@ namespace osu.Game.Rulesets.Osu.Tests
protected class TestDrawableHitCircle : DrawableHitCircle
{
private readonly bool auto;
private readonly double hitOffset;
public TestDrawableHitCircle(HitCircle h, bool auto)
public TestDrawableHitCircle(HitCircle h, bool auto, double hitOffset)
: base(h)
{
this.auto = auto;
this.hitOffset = hitOffset;
}
public void TriggerJudgement() => UpdateResult(true);
public void TriggerJudgement() => Schedule(() => UpdateResult(true));
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (auto && !userTriggered && timeOffset > 0)
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
{
// force success
ApplyResult(r => r.Type = HitResult.Great);
@@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Osu.Tests
Scheduler.AddDelayed(() => comboIndex.Value++, 250, true);
}
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
{
circle.ComboIndexBindable.BindTo(comboIndex);
circle.IndexInCurrentComboBindable.BindTo(comboIndex);
return base.CreateDrawableHitCircle(circle, auto);
return base.CreateDrawableHitCircle(circle, auto, hitOffset);
}
}
}
@@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests
return base.CreateBeatmapForSkinProvider();
}
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto, double hitOffset = 0)
{
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto, hitOffset);
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
@@ -335,8 +335,8 @@ namespace osu.Game.Rulesets.Osu.Tests
var drawable = CreateDrawableSlider(slider);
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawable);
drawable.OnNewResult += onNewResult;
@@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Tests
Scale = new Vector2(0.75f)
};
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawableSpinner);
return drawableSpinner;
}
@@ -7,11 +7,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyAttributes : DifficultyAttributes
{
public double AimStrain;
public double SpeedStrain;
public double ApproachRate;
public double OverallDifficulty;
public int HitCircleCount;
public int SpinnerCount;
public double AimStrain { get; set; }
public double SpeedStrain { get; set; }
public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; }
public int HitCircleCount { get; set; }
public int SpinnerCount { get; set; }
}
}
@@ -41,10 +41,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
@@ -20,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditorRuleset : DrawableOsuRuleset
{
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700;
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods)
{
@@ -46,12 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit
d.ApplyCustomUpdateState += updateState;
}
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle || hitAnimations.Value)
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (hitObject is DrawableHitCircle circle)
{
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension * 4)
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
.Expire();
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
@@ -69,14 +69,20 @@ namespace osu.Game.Rulesets.Osu.Edit
if (hitObject is IHasMainCirclePiece mainPieceContainer)
{
// clear any explode animation logic.
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
// this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables.
ScheduleAfterChildren(() =>
{
if (hitObject.HitObject == null) return;
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true);
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true);
});
}
if (hitObject is DrawableSliderRepeat repeat)
{
repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true);
repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true);
}
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
@@ -93,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire();
break;
}
}
@@ -0,0 +1,12 @@
// 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.
namespace osu.Game.Rulesets.Osu.Mods
{
/// <summary>
/// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
/// </summary>
public interface IMutateApproachCircles
{
}
}
@@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
public override string Description => "Never trust the approach circles...";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)
{
Precision = 0.1f,
MinValue = 2,
MaxValue = 10,
};
[SettingSource("Style", "Change the animation style of the approach circles.", 1)]
public Bindable<AnimationStyle> Style { get; } = new Bindable<AnimationStyle>();
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
drawable.ApplyCustomUpdateState += (drawableObject, state) =>
{
if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;
var hitCircle = drawableHitCircle.HitObject;
drawableHitCircle.ApproachCircle.ClearTransforms(targetMember: nameof(Scale));
using (drawableHitCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
drawableHitCircle.ApproachCircle.ScaleTo(Scale.Value).ScaleTo(1f, hitCircle.TimePreempt, getEasing(Style.Value));
};
}
private Easing getEasing(AnimationStyle style)
{
switch (style)
{
default:
return Easing.None;
case AnimationStyle.Accelerate1:
return Easing.In;
case AnimationStyle.Accelerate2:
return Easing.InCubic;
case AnimationStyle.Accelerate3:
return Easing.InQuint;
case AnimationStyle.Gravity:
return Easing.InBack;
case AnimationStyle.Decelerate1:
return Easing.Out;
case AnimationStyle.Decelerate2:
return Easing.OutCubic;
case AnimationStyle.Decelerate3:
return Easing.OutQuint;
case AnimationStyle.InOut1:
return Easing.InOutCubic;
case AnimationStyle.InOut2:
return Easing.InOutQuint;
}
}
public enum AnimationStyle
{
Gravity,
InOut1,
InOut2,
Accelerate1,
Accelerate2,
Accelerate3,
Decelerate1,
Decelerate2,
Decelerate3,
}
}
}
+9 -13
View File
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -9,22 +8,19 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObjects
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObject
{
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
public void ApplyToDrawableHitObject(DrawableHitObject d)
{
foreach (var d in drawables)
d.OnUpdate += _ =>
{
d.OnUpdate += _ =>
switch (d)
{
switch (d)
{
case DrawableHitCircle circle:
circle.CirclePiece.Rotation = -CurrentRotation;
break;
}
};
}
case DrawableHitCircle circle:
circle.CirclePiece.Rotation = -CurrentRotation;
break;
}
};
}
}
}
@@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Adjust;
public override ModType Type => ModType.DifficultyIncrease;
public override bool Ranked => false;
public override double ScoreMultiplier => 1.12;
private DrawableOsuBlinds blinds;
+12 -16
View File
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@@ -15,7 +14,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
@@ -54,24 +53,21 @@ namespace osu.Game.Rulesets.Osu.Mods
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
}
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
public void ApplyToDrawableHitObject(DrawableHitObject obj)
{
foreach (var obj in drawables)
switch (obj)
{
switch (obj)
{
case DrawableSlider slider:
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
break;
case DrawableSlider slider:
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
break;
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break;
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break;
case DrawableSliderTail tail:
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
break;
}
case DrawableSliderTail tail:
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
break;
}
}
}
@@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
@@ -19,7 +17,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObjects
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
{
public override double ScoreMultiplier => 1.12;
@@ -31,12 +29,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight();
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
foreach (var s in drawables.OfType<DrawableSlider>())
{
if (drawable is DrawableSlider s)
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
}
}
public override void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
@@ -14,7 +14,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModHardRock : ModHardRock, IApplicableToHitObject
{
public override double ScoreMultiplier => 1.06;
public override bool Ranked => true;
public void ApplyToHitObject(HitObject hitObject)
{
+2 -2
View File
@@ -14,12 +14,12 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHidden : ModHidden
public class OsuModHidden : ModHidden, IMutateApproachCircles
{
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) };
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// <summary>
/// Adjusts the size of hit objects during their fade in animation.
/// </summary>
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
{
public override ModType Type => ModType.Fun;
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) };
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@@ -22,7 +22,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRandom : ModRandom, IApplicableToBeatmap
{
public override string Description => "It never gets boring!";
public override bool Ranked => false;
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
+2 -2
View File
@@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSpinIn : ModWithVisibilityAdjustment
public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
{
public override string Name => "Spin In";
public override string Acronym => "SI";
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
// todo: this mod should be able to be compatible with hidden with a bit of further implementation.
public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) };
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
+5 -10
View File
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
@@ -13,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects
public class OsuModSpunOut : Mod, IApplicableToDrawableHitObject
{
public override string Name => "Spun Out";
public override string Acronym => "SO";
@@ -21,18 +20,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{
foreach (var hitObject in drawables)
if (hitObject is DrawableSpinner spinner)
{
if (hitObject is DrawableSpinner spinner)
{
spinner.HandleUserInput = false;
spinner.OnUpdate += onSpinnerUpdate;
}
spinner.HandleUserInput = false;
spinner.OnUpdate += onSpinnerUpdate;
}
}
@@ -13,7 +13,5 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;
public override bool Ranked => true;
}
}
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTraceable : ModWithVisibilityAdjustment
public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
{
public override string Name => "Traceable";
public override string Acronym => "TC";
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@@ -172,6 +172,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateStartTimeStateTransforms();
// always fade out at the circle's start time (to match user expectations).
ApproachCircle.FadeOut(50);
}
@@ -182,6 +183,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut();
// in the case of an early state change, the fade should be expedited to the current point in time.
if (HitStateUpdateTime < HitObject.StartTime)
ApproachCircle.FadeOut(50);
switch (state)
{
case ArmedState.Idle:
+1
View File
@@ -187,6 +187,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(),
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
};
case ModType.System:
@@ -128,5 +128,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner != null)
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
private readonly IBindable<ArmedState> armedState = new Bindable<ArmedState>();
[Resolved]
private DrawableHitObject drawableObject { get; set; }
@@ -54,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
armedState.BindTo(drawableObject.State);
}
protected override void LoadComplete()
@@ -70,19 +68,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
armedState.BindValueChanged(animate, true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}
private void animate(ValueChangedEvent<ArmedState> state)
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
ClearTransforms(true);
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
glow.FadeOut(400);
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
switch (state.NewValue)
switch (state)
{
case ArmedState.Hit:
const double flash_in = 40;
@@ -109,5 +106,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacyMainCirclePiece : CompositeDrawable
{
public override bool RemoveCompletedTransforms => false;
private readonly string priorityLookup;
private readonly bool hasNumber;
@@ -39,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
private readonly IBindable<ArmedState> armedState = new Bindable<ArmedState>();
[Resolved]
private DrawableHitObject drawableObject { get; set; }
@@ -114,7 +115,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
armedState.BindTo(drawableObject.State);
Texture getTextureWithFallback(string name)
{
@@ -140,18 +140,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
armedState.BindValueChanged(animate, true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}
private void animate(ValueChangedEvent<ArmedState> state)
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
const double legacy_fade_duration = 240;
ClearTransforms(true);
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
switch (state.NewValue)
switch (state)
{
case ArmedState.Hit:
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
@@ -176,5 +175,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject != null)
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
@@ -40,6 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-background"),
Colour = source.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SpinnerBackground)?.Value ?? new Color4(100, 100, 100, 255),
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_Y_CENTRE,
},
@@ -7,6 +7,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
SliderTrackOverride,
SliderBorder,
SliderBall
SliderBall,
SpinnerBackground,
}
}
@@ -0,0 +1,12 @@
// 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.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public abstract class TaikoModTestScene : ModTestScene
{
protected sealed override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
}
}
@@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public class TestSceneTaikoModHidden : TaikoModTestScene
{
[Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{
Mod = new TaikoModHidden(),
Autoplay = true,
PassCondition = checkSomeAutoplayHits
});
private bool checkSomeAutoplayHits()
=> Player.ScoreProcessor.JudgedHits >= 4
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
}
}
@@ -7,10 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyAttributes : DifficultyAttributes
{
public double StaminaStrain;
public double RhythmStrain;
public double ColourStrain;
public double ApproachRate;
public double GreatHitWindow;
public double StaminaStrain { get; set; }
public double RhythmStrain { get; set; }
public double ColourStrain { get; set; }
public double ApproachRate { get; set; }
public double GreatHitWindow { get; set; }
}
}
@@ -36,10 +36,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countMeh = Score.Statistics.GetOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetOrDefault(HitResult.Miss);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
@@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
public class TaikoModHardRock : ModHardRock
{
public override double ScoreMultiplier => 1.06;
public override bool Ranked => true;
/// <summary>
/// Multiplier factor added to the scrolling speed.
+72 -2
View File
@@ -1,23 +1,93 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHidden : ModHidden
public class TaikoModHidden : ModHidden, IApplicableToDifficulty
{
public override string Description => @"Beats fade out before you hit them!";
public override double ScoreMultiplier => 1.06;
public override bool HasImplementation => false;
/// <summary>
/// In osu-stable, the hit position is 160, so the active playfield is essentially 160 pixels shorter
/// than the actual screen width. The normalized playfield height is 480, so on a 4:3 screen the
/// playfield ratio of the active area up to the hit position will actually be (640 - 160) / 480 = 1.
/// For custom resolutions/aspect ratios (x:y), the screen width given the normalized height becomes 480 * x / y instead,
/// and the playfield ratio becomes (480 * x / y - 160) / 480 = x / y - 1/3.
/// This constant is equal to the playfield ratio on 4:3 screens divided by the playfield ratio on 16:9 screens.
/// </summary>
private const double hd_sv_scale = (4.0 / 3.0 - 1.0 / 3.0) / (16.0 / 9.0 - 1.0 / 3.0);
private double originalSliderMultiplier;
private ControlPointInfo controlPointInfo;
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
ApplyNormalVisibilityState(hitObject, state);
}
protected double MultiplierAt(double position)
{
double beatLength = controlPointInfo.TimingPointAt(position).BeatLength;
double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier;
return originalSliderMultiplier * speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
}
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
switch (hitObject)
{
case DrawableDrumRollTick _:
case DrawableHit _:
double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime);
double start = hitObject.HitObject.StartTime - preempt * 0.6;
double duration = preempt * 0.3;
using (hitObject.BeginAbsoluteSequence(start))
{
hitObject.FadeOut(duration);
// DrawableHitObject sets LifetimeEnd to LatestTransformEndTime if it isn't manually changed.
// in order for the object to not be killed before its actual end time (as the latest transform ends earlier), set lifetime end explicitly.
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
: hitObject.HitStateUpdateTime;
}
break;
}
}
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
{
}
public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
// needs to be read after all processing has been run (TaikoBeatmapConverter applies an adjustment which would otherwise be omitted).
originalSliderMultiplier = difficulty.SliderMultiplier;
// osu-stable has an added playfield cover that essentially forces a 4:3 playfield ratio, by cutting off all objects past that size.
// This is not yet implemented; instead a playfield adjustment container is present which maintains a 16:9 ratio.
// For now, increase the slider multiplier proportionally so that the notes stay on the screen for the same amount of time as on stable.
// Note that this means that the notes will scroll faster as they have a longer distance to travel on the screen in that same amount of time.
difficulty.SliderMultiplier /= hd_sv_scale;
}
public override void ApplyToBeatmap(IBeatmap beatmap)
{
controlPointInfo = beatmap.ControlPointInfo;
}
}
}
+15 -2
View File
@@ -3,14 +3,19 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class Hit : TaikoStrongableHitObject
public class Hit : TaikoStrongableHitObject, IHasDisplayColour
{
public readonly Bindable<HitType> TypeBindable = new Bindable<HitType>();
public Bindable<Color4> DisplayColour { get; } = new Bindable<Color4>(COLOUR_CENTRE);
/// <summary>
/// The <see cref="HitType"/> that actuates this <see cref="Hit"/>.
/// </summary>
@@ -20,9 +25,17 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => TypeBindable.Value = value;
}
public static readonly Color4 COLOUR_CENTRE = Color4Extensions.FromHex(@"bb1177");
public static readonly Color4 COLOUR_RIM = Color4Extensions.FromHex(@"2299bb");
public Hit()
{
TypeBindable.BindValueChanged(_ => updateSamplesFromType());
TypeBindable.BindValueChanged(_ =>
{
updateSamplesFromType();
DisplayColour.Value = Type == HitType.Centre ? COLOUR_CENTRE : COLOUR_RIM;
});
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples());
}
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.PinkDarker;
AccentColour = Hit.COLOUR_CENTRE;
}
/// <summary>
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK;
using osuTK.Graphics;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.BlueDarker;
AccentColour = Hit.COLOUR_RIM;
}
/// <summary>
@@ -113,7 +113,6 @@ namespace osu.Game.Tests.Collections.IO
await importCollectionsFromStream(osu, ms);
}
Assert.That(host.UpdateThread.Running, Is.True);
Assert.That(exceptionThrown, Is.False);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0));
}
@@ -44,11 +44,9 @@ namespace osu.Game.Tests.Gameplay
{
TestDrawableHitObject dho = null;
TestLifetimeEntry entry = null;
AddStep("Create DHO", () =>
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
{
dho = new TestDrawableHitObject(null);
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
Child = dho;
Entry = entry = new TestLifetimeEntry(new HitObject())
});
AddStep("KeepAlive = true", () =>
@@ -81,12 +79,10 @@ namespace osu.Game.Tests.Gameplay
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
TestDrawableHitObject dho = null;
AddStep("Create DHO", () =>
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
{
dho = new TestDrawableHitObject(null);
dho.Apply(entry);
Child = dho;
dho.SetLifetimeStartOnApply = true;
Entry = entry,
SetLifetimeStartOnApply = true
});
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
@@ -97,11 +93,9 @@ namespace osu.Game.Tests.Gameplay
{
TestDrawableHitObject dho = null;
TestLifetimeEntry entry = null;
AddStep("Create DHO", () =>
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
{
dho = new TestDrawableHitObject(null);
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
Child = dho;
Entry = entry = new TestLifetimeEntry(new HitObject())
});
AddStep("Set entry lifetime", () =>
@@ -135,7 +129,7 @@ namespace osu.Game.Tests.Gameplay
public bool SetLifetimeStartOnApply;
public TestDrawableHitObject(HitObject hitObject)
public TestDrawableHitObject(HitObject hitObject = null)
: base(hitObject)
{
}
+28 -2
View File
@@ -21,6 +21,14 @@ namespace osu.Game.Tests.Mods
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
}
[Test]
public void TestModIsCompatibleByItselfWithIncompatibleInterface()
{
var mod = new Mock<CustomMod1>();
mod.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
}
[Test]
public void TestIncompatibleThroughTopLevel()
{
@@ -34,6 +42,20 @@ namespace osu.Game.Tests.Mods
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
}
[Test]
public void TestIncompatibleThroughInterface()
{
var mod1 = new Mock<CustomMod1>();
var mod2 = new Mock<CustomMod2>();
mod1.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
// Test both orderings.
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False);
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
}
[Test]
public void TestMultiModIncompatibleWithTopLevel()
{
@@ -149,11 +171,15 @@ namespace osu.Game.Tests.Mods
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
public abstract class CustomMod1 : Mod
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
{
}
public abstract class CustomMod2 : Mod
public abstract class CustomMod2 : Mod, IModCompatibilitySpecification
{
}
public interface IModCompatibilitySpecification
{
}
}
@@ -33,10 +33,11 @@ namespace osu.Game.Tests.NonVisual.Filtering
* outside of the range.
*/
[Test]
public void TestApplyStarQueries()
[TestCase("star")]
[TestCase("stars")]
public void TestApplyStarQueries(string variant)
{
const string query = "stars<4 easy";
string query = $"{variant}<4 easy";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
+1 -1
View File
@@ -1,2 +1,2 @@
[General]
Version: 1.0
// no version specified means v1
Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

@@ -0,0 +1,116 @@
// 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.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Skins
{
[TestFixture]
[HeadlessTest]
public class TestSceneBeatmapSkinLookupDisables : OsuTestScene
{
private UserSkinSource userSource;
private BeatmapSkinSource beatmapSource;
private SkinRequester requester;
[Resolved]
private OsuConfigManager config { get; set; }
[SetUp]
public void SetUp() => Schedule(() =>
{
Add(new SkinProvidingContainer(userSource = new UserSkinSource())
.WithChild(new BeatmapSkinProvidingContainer(beatmapSource = new BeatmapSkinSource())
.WithChild(requester = new SkinRequester())));
});
[TestCase(false)]
[TestCase(true)]
public void TestDrawableLookup(bool allowBeatmapLookups)
{
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups));
string expected = allowBeatmapLookups ? "beatmap" : "user";
AddAssert($"Check lookup is from {expected}", () => requester.GetDrawableComponent(new TestSkinComponent())?.Name == expected);
}
[TestCase(false)]
[TestCase(true)]
public void TestProviderLookup(bool allowBeatmapLookups)
{
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups));
ISkin expected() => allowBeatmapLookups ? (ISkin)beatmapSource : userSource;
AddAssert("Check lookup is from correct source", () => requester.FindProvider(s => s.GetDrawableComponent(new TestSkinComponent()) != null) == expected());
}
public class UserSkinSource : LegacySkin
{
public UserSkinSource()
: base(new SkinInfo(), null, null, string.Empty)
{
}
public override Drawable GetDrawableComponent(ISkinComponent component)
{
return new Container { Name = "user" };
}
}
public class BeatmapSkinSource : LegacyBeatmapSkin
{
public BeatmapSkinSource()
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
{
}
public override Drawable GetDrawableComponent(ISkinComponent component)
{
return new Container { Name = "beatmap" };
}
}
public class SkinRequester : Drawable, ISkin
{
private ISkinSource skin;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
this.skin = skin;
}
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => skin.FindProvider(lookupFunction);
}
private class TestSkinComponent : ISkinComponent
{
public string LookupName => string.Empty;
}
}
}
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -11,6 +12,7 @@ using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Background
@@ -23,6 +25,9 @@ namespace osu.Game.Tests.Visual.Background
private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType<Graphics.Backgrounds.Background>().FirstOrDefault();
[Resolved]
private SkinManager skins { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
@@ -35,7 +40,7 @@ namespace osu.Game.Tests.Visual.Background
}
[Test]
public void TestTogglingStoryboardSwitchesBackgroundType()
public void TestBackgroundTypeSwitch()
{
setSupporter(true);
@@ -44,6 +49,12 @@ namespace osu.Game.Tests.Visual.Background
setSourceMode(BackgroundSource.BeatmapWithStoryboard);
AddUntilStep("is storyboard background", () => getCurrentBackground() is BeatmapBackgroundWithStoryboard);
setSourceMode(BackgroundSource.Skin);
AddUntilStep("is default background", () => getCurrentBackground().GetType() == typeof(Graphics.Backgrounds.Background));
setCustomSkin();
AddUntilStep("is skin background", () => getCurrentBackground() is SkinBackground);
}
[Test]
@@ -61,15 +72,19 @@ namespace osu.Game.Tests.Visual.Background
AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground);
}
[Test]
public void TestBeatmapDoesntReloadOnNoChange()
[TestCase(BackgroundSource.Beatmap, typeof(BeatmapBackground))]
[TestCase(BackgroundSource.BeatmapWithStoryboard, typeof(BeatmapBackgroundWithStoryboard))]
[TestCase(BackgroundSource.Skin, typeof(SkinBackground))]
public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType)
{
BeatmapBackground last = null;
Graphics.Backgrounds.Background last = null;
setSourceMode(BackgroundSource.Beatmap);
setSourceMode(source);
setSupporter(true);
if (source == BackgroundSource.Skin)
setCustomSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground() as BeatmapBackground) != null);
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == backgroundType);
AddAssert("next doesn't load new background", () => screen.Next() == false);
// doesn't really need to be checked but might as well.
@@ -77,8 +92,25 @@ namespace osu.Game.Tests.Visual.Background
AddUntilStep("ensure same background instance", () => last == getCurrentBackground());
}
[Test]
public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter)
{
Graphics.Backgrounds.Background last = null;
setSourceMode(BackgroundSource.Skin);
setSupporter(supporter);
setDefaultSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
AddAssert("next cycles background", () => screen.Next());
// doesn't really need to be checked but might as well.
AddWaitStep("wait a bit", 5);
AddUntilStep("ensure different background instance", () => last != getCurrentBackground());
}
private void setSourceMode(BackgroundSource source) =>
AddStep("set background mode to beatmap", () => config.SetValue(OsuSetting.MenuBackgroundSource, source));
AddStep($"set background mode to {source}", () => config.SetValue(OsuSetting.MenuBackgroundSource, source));
private void setSupporter(bool isSupporter) =>
AddStep($"set supporter {isSupporter}", () => ((DummyAPIAccess)API).LocalUser.Value = new User
@@ -86,5 +118,16 @@ namespace osu.Game.Tests.Visual.Background
IsSupporter = isSupporter,
Id = API.LocalUser.Value.Id + 1,
});
private void setCustomSkin()
{
// feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin.
AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo { ID = 5 });
}
private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault());
[TearDownSteps]
public void TearDown() => setDefaultSkin();
}
}
@@ -161,15 +161,18 @@ namespace osu.Game.Tests.Visual.Background
private void loadNextBackground()
{
SeasonalBackground previousBackground = null;
SeasonalBackground background = null;
AddStep("create next background", () =>
{
previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault();
background = backgroundLoader.LoadNextBackground();
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
});
AddUntilStep("background loaded", () => background.IsLoaded);
AddAssert("background is different", () => !background.Equals(previousBackground));
}
private void assertAnyBackground()
@@ -0,0 +1,144 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneMetadataSection : OsuTestScene
{
[Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap());
private TestMetadataSection metadataSection;
[Test]
public void TestMinimalMetadata()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.Artist = "Example Artist";
editorBeatmap.Metadata.ArtistUnicode = null;
editorBeatmap.Metadata.Title = "Example Title";
editorBeatmap.Metadata.TitleUnicode = null;
});
createSection();
assertArtist("Example Artist");
assertRomanisedArtist("Example Artist", false);
assertTitle("Example Title");
assertRomanisedTitle("Example Title", false);
}
[Test]
public void TestInitialisationFromNonRomanisedVariant()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.ArtistUnicode = "*なみりん";
editorBeatmap.Metadata.Artist = null;
editorBeatmap.Metadata.TitleUnicode = "コイシテイク・プラネット";
editorBeatmap.Metadata.Title = null;
});
createSection();
assertArtist("*なみりん");
assertRomanisedArtist(string.Empty, true);
assertTitle("コイシテイク・プラネット");
assertRomanisedTitle(string.Empty, true);
}
[Test]
public void TestInitialisationPreservesOriginalValues()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.ArtistUnicode = "*なみりん";
editorBeatmap.Metadata.Artist = "*namirin";
editorBeatmap.Metadata.TitleUnicode = "コイシテイク・プラネット";
editorBeatmap.Metadata.Title = "Koishiteiku Planet";
});
createSection();
assertArtist("*なみりん");
assertRomanisedArtist("*namirin", true);
assertTitle("コイシテイク・プラネット");
assertRomanisedTitle("Koishiteiku Planet", true);
}
[Test]
public void TestValueTransfer()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.ArtistUnicode = "*なみりん";
editorBeatmap.Metadata.Artist = null;
editorBeatmap.Metadata.TitleUnicode = "コイシテイク・プラネット";
editorBeatmap.Metadata.Title = null;
});
createSection();
AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin");
assertArtist("*namirin");
assertRomanisedArtist("*namirin", false);
AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん");
assertArtist("*なみりん");
assertRomanisedArtist("*namirin", true);
AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori");
assertTitle("Hitokoto no kyori");
assertRomanisedTitle("Hitokoto no kyori", false);
AddStep("set native title", () => metadataSection.TitleTextBox.Current.Value = "ヒトコトの距離");
assertTitle("ヒトコトの距離");
assertRomanisedTitle("Hitokoto no kyori", true);
}
private void createSection()
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
private void assertArtist(string expected)
=> AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected);
private void assertRomanisedArtist(string expected, bool editable)
{
AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected);
AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable);
}
private void assertTitle(string expected)
=> AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected);
private void assertRomanisedTitle(string expected, bool editable)
{
AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected);
AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable);
}
private class TestMetadataSection : MetadataSection
{
public new LabelledTextBox ArtistTextBox => base.ArtistTextBox;
public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
public new LabelledTextBox TitleTextBox => base.TitleTextBox;
public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
}
}
}

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