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

Compare commits

...

1038 Commits

505 changed files with 9092 additions and 4548 deletions
+7
View File
@@ -10,6 +10,10 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
### Cake ###
tools/*
!tools/cakebuild.csproj
# Build results
bin/[Dd]ebug/
[Dd]ebugPublic/
@@ -98,6 +102,7 @@ $tf/
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
inspectcode
# JustCode is a .NET coding add-in
.JustCode
@@ -257,3 +262,5 @@ paket-files/
__pycache__/
*.pyc
Staging/
inspectcodereport.xml
+4 -1
View File
@@ -24,7 +24,10 @@ Clone the repository including submodules
Build and run
- Using Visual Studio 2017, Rider or Visual Studio Code (configurations are included)
- From command line using `dotnet run --project osu.Desktop`
- From command line using `dotnet run --project osu.Desktop`. When building for non-development purposes, add `-c Release` to gain higher performance.
- To run with code analysis, instead use `powershell ./build.ps1` or `build.sh`. This is currently only supported under windows due to [resharper cli shortcomings](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternative, you can install resharper or use rider to get inline support in your IDE of choice.
Note: If you run from command line under linux, you will need to prefix the output folder to your `LD_LIBRARY_PATH`. See `.vscode/launch.json` for an example
If you run into issues building you may need to restore nuget packages (commonly via `dotnet restore`). Visual Studio Code users must run `Restore` task from debug tab before attempt to build.
+3 -17
View File
@@ -1,22 +1,8 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2017
configuration: Debug
cache:
- C:\ProgramData\chocolatey\bin -> appveyor.yml
- C:\ProgramData\chocolatey\lib -> appveyor.yml
test: off
install:
- cmd: git submodule update --init --recursive --depth=5
- cmd: choco install resharper-clt -y
- cmd: choco install nvika -y
- cmd: dotnet tool install CodeFileSanity --version 0.0.16 --global
before_build:
- cmd: CodeFileSanity
- cmd: nuget restore -verbosity quiet
build:
project: osu.sln
parallel: true
verbosity: minimal
after_build:
- cmd: inspectcode --o="inspectcodereport.xml" --projects:osu.Game* --caches-home="inspectcode" osu.sln > NUL
- cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors
build_script:
- cmd: PowerShell -Version 2.0 .\build.ps1
-30
View File
@@ -1,30 +0,0 @@
clone_depth: 1
version: '{build}'
skip_non_tags: true
image: Visual Studio 2017
install:
- git clone https://github.com/ppy/osu-deploy
before_build:
- ps: if($env:appveyor_repo_tag -eq 'True') { Update-AppveyorBuild -Version $env:appveyor_repo_tag_name }
- cmd: git submodule update --init --recursive --depth=5
- cmd: nuget restore -verbosity quiet
build_script:
- ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1'))
- appveyor DownloadFile https://puu.sh/A6g5K/4d08705438.enc # signing certificate
- cmd: appveyor-tools\secure-file -decrypt 4d08705438.enc -secret %decode_secret% -out %HOMEPATH%\deanherbert.pfx
- appveyor DownloadFile https://puu.sh/A6g75/fdc6f19b04.enc # deploy configuration
- cd osu-deploy
- nuget restore -verbosity quiet
- msbuild osu.Desktop.Deploy.csproj
- cmd: ..\appveyor-tools\secure-file -decrypt ..\fdc6f19b04.enc -secret %decode_secret% -out bin\Debug\netcoreapp2.1\osu.Desktop.Deploy.dll.config
- dotnet bin/Debug/netcoreapp2.1/osu.Desktop.Deploy.dll %code_signing_password% %APPVEYOR_REPO_TAG_NAME%
environment:
decode_secret:
secure: i67IC2xj6DjjxmA6Oj2jing3+MwzLkq6CbGsjfZ7rdY=
code_signing_password:
secure: 34tLNqvjmmZEi97MLKfrnQ==
artifacts:
- path: 'osu-deploy/releases/*'
deploy:
- provider: Environment
name: github
+72
View File
@@ -0,0 +1,72 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.21"
#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2018.2.2"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS
///////////////////////////////////////////////////////////////////////////////
var target = Argument("target", "Build");
var configuration = Argument("configuration", "Release");
var osuSolution = new FilePath("./osu.sln");
///////////////////////////////////////////////////////////////////////////////
// TASKS
///////////////////////////////////////////////////////////////////////////////
Task("Restore")
.Does(() => {
DotNetCoreRestore(osuSolution.FullPath);
});
Task("Compile")
.IsDependentOn("Restore")
.Does(() => {
DotNetCoreBuild(osuSolution.FullPath, new DotNetCoreBuildSettings {
Configuration = configuration,
NoRestore = true,
});
});
Task("Test")
.IsDependentOn("Compile")
.Does(() => {
var testAssemblies = GetFiles("**/*.Tests/bin/**/*.Tests.dll");
DotNetCoreVSTest(testAssemblies, new DotNetCoreVSTestSettings {
Logger = AppVeyor.IsRunningOnAppVeyor ? "Appveyor" : $"trx",
Parallel = true,
ToolTimeout = TimeSpan.FromMinutes(10),
});
});
// windows only because both inspectcore and nvika depend on net45
Task("InspectCode")
.WithCriteria(IsRunningOnWindows())
.IsDependentOn("Compile")
.Does(() => {
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
InspectCode(osuSolution, new InspectCodeSettings {
CachesHome = "inspectcode",
OutputFile = "inspectcodereport.xml",
});
StartProcess(nVikaToolPath, @"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors");
});
Task("CodeFileSanity")
.Does(() => {
ValidateCodeSanity(new ValidateCodeSanitySettings {
RootDirectory = ".",
IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
});
});
Task("Build")
.IsDependentOn("CodeFileSanity")
.IsDependentOn("InspectCode")
.IsDependentOn("Test");
RunTarget(target);
+79
View File
@@ -0,0 +1,79 @@
##########################################################################
# This is a customized Cake bootstrapper script for PowerShell.
##########################################################################
<#
.SYNOPSIS
This is a Powershell script to bootstrap a Cake build.
.DESCRIPTION
This Powershell script restores NuGet tools (including Cake)
and execute your Cake build script with the parameters you provide.
.PARAMETER Script
The build script to execute.
.PARAMETER Target
The build script target to run.
.PARAMETER Configuration
The build configuration to use.
.PARAMETER Verbosity
Specifies the amount of information to be displayed.
.PARAMETER ShowDescription
Shows description about tasks.
.PARAMETER DryRun
Performs a dry run.
.PARAMETER ScriptArgs
Remaining arguments are added here.
.LINK
https://cakebuild.net
#>
[CmdletBinding()]
Param(
[string]$Script = "build.cake",
[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
)
Write-Host "Preparing to run build script..."
# Determine the script root for resolving other paths.
if(!$PSScriptRoot){
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
}
# Resolve the paths for resources used for debugging.
$TOOLS_DIR = Join-Path $PSScriptRoot "tools"
$CAKE_CSPROJ = Join-Path $TOOLS_DIR "cakebuild.csproj"
# Install the required tools locally.
Write-Host "Restoring cake tools..."
Invoke-Expression "dotnet restore `"$CAKE_CSPROJ`" --packages `"$TOOLS_DIR`"" | Out-Null
# Find the Cake executable
$CAKE_EXECUTABLE = (Get-ChildItem -Path ./tools/cake.coreclr/ -Filter Cake.dll -Recurse).FullName
# Build Cake arguments
$cakeArguments = @("$Script");
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
# Start Cake
Write-Host "Running build script..."
Invoke-Expression "dotnet `"$CAKE_EXECUTABLE`" $cakeArguments"
exit $LASTEXITCODE
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
##########################################################################
# This is a customized Cake bootstrapper script for Shell.
##########################################################################
echo "Preparing to run build script..."
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
TOOLS_DIR=$SCRIPT_DIR/tools
CAKE_BINARY_PATH=$TOOLS_DIR/"cake.coreclr"
SCRIPT="build.cake"
CAKE_CSPROJ=$TOOLS_DIR/"cakebuild.csproj"
# Parse arguments.
CAKE_ARGUMENTS=()
for i in "$@"; do
case $1 in
-s|--script) SCRIPT="$2"; shift ;;
--) shift; CAKE_ARGUMENTS+=("$@"); break ;;
*) CAKE_ARGUMENTS+=("$1") ;;
esac
shift
done
# Install the required tools locally.
echo "Restoring cake tools..."
dotnet restore $CAKE_CSPROJ --packages $TOOLS_DIR > /dev/null 2>&1
# Search for the CakeBuild binary.
CAKE_BINARY=$(find $CAKE_BINARY_PATH -name "Cake.dll")
# Start Cake
echo "Running build script..."
dotnet "$CAKE_BINARY" $SCRIPT "${CAKE_ARGUMENTS[@]}"
+5
View File
@@ -0,0 +1,5 @@
[Nuget]
Source=https://api.nuget.org/v3/index.json
UseInProcessClient=true
LoadDependencies=true
-3
View File
@@ -27,9 +27,6 @@ namespace osu.Desktop.Overlays
private NotificationOverlay notificationOverlay;
private GameHost host;
public override bool HandleKeyboardInput => false;
public override bool HandleMouseInput => false;
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config, GameHost host)
{
+25
View File
@@ -4,7 +4,11 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Development;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IPC;
@@ -20,6 +24,8 @@ namespace osu.Desktop
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
{
host.ExceptionThrown += handleException;
if (!host.IsPrimaryInstance)
{
var importer = new ArchiveImportIPCChannel(host);
@@ -45,5 +51,24 @@ namespace osu.Desktop
return 0;
}
}
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
/// <summary>
/// Allow a maximum of one unhandled exception, per second of execution.
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return continueExecution;
}
}
}
+3 -3
View File
@@ -27,9 +27,9 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.5.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.1" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.4" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
@@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@@ -38,12 +37,12 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(new JuiceStream
{
X = 0.5f - width / 2,
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)
},
CurveType = CurveType.Linear,
PathType = PathType.Linear,
Distance = width * CatchPlayfield.BASE_WIDTH,
StartTime = i * 2000,
NewCombo = i % 8 == 0
@@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -35,12 +35,13 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
ControlPoints = curveData.ControlPoints,
CurveType = curveData.CurveType,
PathType = curveData.PathType,
Distance = curveData.Distance,
RepeatSamples = curveData.RepeatSamples,
RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0
};
}
@@ -51,7 +52,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
};
}
else
@@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
};
}
@@ -74,42 +74,42 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private void initialiseHyperDash(List<CatchHitObject> objects)
{
// todo: add difficulty adjust.
double halfCatcherWidth = CatcherArea.CATCHER_SIZE * (objects.FirstOrDefault()?.Scale ?? 1) / CatchPlayfield.BASE_WIDTH / 2;
List<CatchHitObject> objectWithDroplets = new List<CatchHitObject>();
foreach (var currentObject in objects)
{
if (currentObject is Fruit)
objectWithDroplets.Add(currentObject);
if (currentObject is JuiceStream)
foreach (var currentJuiceElement in currentObject.NestedHitObjects)
if (!(currentJuiceElement is TinyDroplet))
objectWithDroplets.Add((CatchHitObject)currentJuiceElement);
}
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = CatcherArea.GetCatcherSize(Beatmap.BeatmapInfo.BaseDifficulty) / 2;
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
int objCount = objects.Count;
for (int i = 0; i < objCount - 1; i++)
for (int i = 0; i < objectWithDroplets.Count - 1; i++)
{
CatchHitObject currentObject = objects[i];
// not needed?
// if (currentObject is TinyDroplet) continue;
CatchHitObject nextObject = objects[i + 1];
// while (nextObject is TinyDroplet)
// {
// if (++i == objCount - 1) break;
// nextObject = objects[i + 1];
// }
CatchHitObject currentObject = objectWithDroplets[i];
CatchHitObject nextObject = objectWithDroplets[i + 1];
int thisDirection = nextObject.X > currentObject.X ? 1 : -1;
double timeToNext = nextObject.StartTime - ((currentObject as IHasEndTime)?.EndTime ?? currentObject.StartTime) - 4;
double timeToNext = nextObject.StartTime - currentObject.StartTime;
double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
if (timeToNext * CatcherArea.Catcher.BASE_SPEED < distanceToNext)
float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext);
if (distanceToHyper < 0)
{
currentObject.HyperDashTarget = nextObject;
lastExcess = halfCatcherWidth;
}
else
{
//currentObject.DistanceToHyperDash = timeToNext - distanceToNext;
lastExcess = MathHelper.Clamp(timeToNext - distanceToNext, 0, halfCatcherWidth);
currentObject.DistanceToHyperDash = distanceToHyper;
lastExcess = MathHelper.Clamp(distanceToHyper, 0, halfCatcherWidth);
}
lastDirection = thisDirection;
@@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Judgements
@@ -9,8 +10,6 @@ namespace osu.Game.Rulesets.Catch.Judgements
{
public override bool AffectsCombo => false;
public override bool ShouldExplode => true;
protected override int NumericResultFor(HitResult result)
{
switch (result)
@@ -32,5 +31,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 8;
}
}
public override bool ShouldExplodeFor(JudgementResult result) => true;
}
}
@@ -23,21 +23,10 @@ namespace osu.Game.Rulesets.Catch.Judgements
}
/// <summary>
/// The base health increase for the result achieved.
/// Retrieves the numeric health increase of a <see cref="HitResult"/>.
/// </summary>
public float HealthIncrease => HealthIncreaseFor(Result);
/// <summary>
/// Whether fruit on the platter should explode or drop.
/// Note that this is only checked if the owning object is also <see cref="IHasComboInformation.LastInCombo" />
/// </summary>
public virtual bool ShouldExplode => IsHit;
/// <summary>
/// Convert a <see cref="HitResult"/> to a base health increase.
/// </summary>
/// <param name="result">The value to convert.</param>
/// <returns>The base health increase.</returns>
/// <param name="result">The <see cref="HitResult"/> to find the numeric health increase for.</param>
/// <returns>The numeric health increase of <paramref name="result"/>.</returns>
protected virtual float HealthIncreaseFor(HitResult result)
{
switch (result)
@@ -48,5 +37,18 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 10.2f;
}
}
/// <summary>
/// Retrieves the numeric health increase of a <see cref="JudgementResult"/>.
/// </summary>
/// <param name="result">The <see cref="JudgementResult"/> to find the numeric health increase for.</param>
/// <returns>The numeric health increase of <paramref name="result"/>.</returns>
public float HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type);
/// <summary>
/// Whether fruit on the platter should explode or drop.
/// Note that this is only checked if the owning object is also <see cref="IHasComboInformation.LastInCombo" />
/// </summary>
public virtual bool ShouldExplodeFor(JudgementResult result) => result.IsHit;
}
}
@@ -1,10 +1,15 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
public class Banana : Fruit
{
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement();
}
}
@@ -16,16 +16,20 @@ namespace osu.Game.Rulesets.Catch.Objects
public int IndexInBeatmap { get; set; }
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4);
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(ComboIndex % 4);
public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; }
public int IndexInCurrentCombo { get; set; }
public int ComboIndex { get; set; }
/// <summary>
/// The distance for a fruit to to next hyper if it's not a hyper.
/// Difference between the distance to the next object
/// and the distance that would have triggered a hyper dash.
/// A value close to 0 indicates a difficult jump (for difficulty calculation).
/// </summary>
public float DistanceToHyperDash { get; set; }
@@ -1,8 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Catch.Judgements;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableBanana : DrawableFruit
@@ -11,7 +9,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
: base(h)
{
}
protected override CatchJudgement CreateJudgement() => new CatchBananaJudgement();
}
}
@@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
AddNested(getVisualRepresentation?.Invoke(b));
}
protected override bool ProvidesJudgement => false;
protected override void AddNested(DrawableHitObject h)
{
((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false;
@@ -5,7 +5,6 @@ using System;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -53,20 +52,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
public Func<CatchHitObject, bool> CheckPosition;
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (CheckPosition == null) return;
if (timeOffset >= 0)
{
var judgement = CreateJudgement();
judgement.Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss;
AddJudgement(judgement);
}
if (timeOffset >= 0 && Result != null)
ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss);
}
protected virtual CatchJudgement CreateJudgement() => new CatchJudgement();
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
@@ -6,7 +6,6 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
using OpenTK;
using OpenTK.Graphics;
using osu.Game.Rulesets.Catch.Judgements;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
@@ -24,8 +23,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
Masking = false;
}
protected override CatchJudgement CreateJudgement() => new CatchDropletJudgement();
[BackgroundDependencyLoader]
private void load()
{
@@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
AddNested(getVisualRepresentation?.Invoke(o));
}
protected override bool ProvidesJudgement => false;
protected override void AddNested(DrawableHitObject h)
{
var catchObject = (DrawableCatchHitObject)h;
@@ -2,18 +2,15 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Game.Rulesets.Catch.Judgements;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableTinyDroplet : DrawableDroplet
{
public DrawableTinyDroplet(Droplet h)
public DrawableTinyDroplet(TinyDroplet h)
: base(h)
{
Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS) / 8;
}
protected override CatchJudgement CreateJudgement() => new CatchTinyDropletJudgement();
}
}
@@ -1,9 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
public class Droplet : CatchHitObject
{
public override Judgement CreateJudgement() => new CatchDropletJudgement();
}
}
+4
View File
@@ -1,9 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
public class Fruit : CatchHitObject
{
public override Judgement CreateJudgement() => new CatchJudgement();
}
}
+14 -14
View File
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Objects
if (TickDistance == 0)
return;
var length = Curve.Distance;
var length = Path.Distance;
var tickDistance = Math.Min(TickDistance, length);
var spanDuration = length / Velocity;
@@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new TinyDroplet
{
StartTime = t,
X = X + Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
X = X + Path.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
@@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new Droplet
{
StartTime = time,
X = X + Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
X = X + Path.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
@@ -127,12 +127,12 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = Samples,
StartTime = spanStartTime + spanDuration,
X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
X = X + Path.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
});
}
}
public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
@@ -140,24 +140,24 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Distance
{
get { return Curve.Distance; }
set { Curve.Distance = value; }
get { return Path.Distance; }
set { Path.Distance = value; }
}
public SliderCurve Curve { get; } = new SliderCurve();
public SliderPath Path { get; } = new SliderPath();
public List<Vector2> ControlPoints
public Vector2[] ControlPoints
{
get { return Curve.ControlPoints; }
set { Curve.ControlPoints = value; }
get { return Path.ControlPoints; }
set { Path.ControlPoints = value; }
}
public List<List<SampleInfo>> RepeatSamples { get; set; } = new List<List<SampleInfo>>();
public CurveType CurveType
public PathType PathType
{
get { return Curve.CurveType; }
set { Curve.CurveType = value; }
get { return Path.PathType; }
set { Path.PathType = value; }
}
public double? LegacyLastTickOffset { get; set; }
@@ -1,9 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Catch.Objects
{
public class TinyDroplet : Droplet
{
public override Judgement CreateJudgement() => new CatchTinyDropletJudgement();
}
}
@@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
@@ -21,55 +20,28 @@ namespace osu.Game.Rulesets.Catch.Scoring
private float hpDrainRate;
protected override void SimulateAutoplay(Beatmap<CatchHitObject> beatmap)
protected override void ApplyBeatmap(Beatmap<CatchHitObject> beatmap)
{
hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate;
base.ApplyBeatmap(beatmap);
foreach (var obj in beatmap.HitObjects)
{
switch (obj)
{
case JuiceStream stream:
foreach (var nestedObject in stream.NestedHitObjects)
switch (nestedObject)
{
case TinyDroplet _:
AddJudgement(new CatchTinyDropletJudgement { Result = HitResult.Perfect });
break;
case Droplet _:
AddJudgement(new CatchDropletJudgement { Result = HitResult.Perfect });
break;
case Fruit _:
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
break;
}
break;
case BananaShower shower:
foreach (var _ in shower.NestedHitObjects.Cast<CatchHitObject>())
AddJudgement(new CatchBananaJudgement { Result = HitResult.Perfect });
break;
case Fruit _:
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
break;
}
}
hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate;
}
private const double harshness = 0.01;
protected override void OnNewJudgement(Judgement judgement)
protected override void ApplyResult(JudgementResult result)
{
base.OnNewJudgement(judgement);
base.ApplyResult(result);
if (judgement.Result == HitResult.Miss)
if (result.Type == HitResult.Miss)
{
if (!judgement.IsBonus)
if (!result.Judgement.IsBonus)
Health.Value -= hpDrainRate * (harshness * 2);
return;
}
if (judgement is CatchJudgement catchJudgement)
Health.Value += Math.Max(catchJudgement.HealthIncrease - hpDrainRate, 0) * harshness;
if (result.Judgement is CatchJudgement catchJudgement)
Health.Value += Math.Max(catchJudgement.HealthIncreaseFor(result) - hpDrainRate, 0) * harshness;
}
}
}
+29 -24
View File
@@ -5,11 +5,13 @@ using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -17,13 +19,13 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float BASE_WIDTH = 512;
protected override Container<Drawable> Content => content;
private readonly Container<Drawable> content;
private readonly CatcherArea catcherArea;
protected override bool UserScrollSpeedAdjustment => false;
protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Constant;
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> getVisualRepresentation)
: base(BASE_WIDTH)
{
Direction.Value = ScrollingDirection.Down;
@@ -32,34 +34,36 @@ namespace osu.Game.Rulesets.Catch.UI
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
base.Content.Anchor = Anchor.BottomLeft;
base.Content.Origin = Anchor.BottomLeft;
Size = new Vector2(0.86f); // matches stable's vertical offset for catcher plate
base.Content.AddRange(new Drawable[]
InternalChild = new PlayfieldAdjustmentContainer
{
explodingFruitContainer = new Container
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
},
catcherArea = new CatcherArea(difficulty)
{
GetVisualRepresentation = getVisualRepresentation,
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
},
content = new Container<Drawable>
{
RelativeSizeAxes = Axes.Both,
},
});
explodingFruitContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
catcherArea = new CatcherArea(difficulty)
{
GetVisualRepresentation = getVisualRepresentation,
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
},
HitObjectContainer
}
};
VisibleTimeRange.Value = BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
}
public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.AttemptCatch(obj);
public override void Add(DrawableHitObject h)
{
h.OnJudgement += onJudgement;
h.OnNewResult += onNewResult;
base.Add(h);
@@ -67,6 +71,7 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.CheckPosition = CheckIfWeCanCatch;
}
private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) => catcherArea.OnJudgement((DrawableCatchHitObject)judgedObject, judgement);
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> catcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
}
}
@@ -13,7 +13,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -32,9 +31,7 @@ namespace osu.Game.Rulesets.Catch.UI
public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
protected override Vector2 PlayfieldArea => new Vector2(0.86f); // matches stable's vertical offset for catcher plate
protected override DrawableHitObject<CatchHitObject> GetVisualRepresentation(CatchHitObject h)
public override DrawableHitObject<CatchHitObject> GetVisualRepresentation(CatchHitObject h)
{
switch (h)
{
+15 -12
View File
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherArea : Container
{
public const float CATCHER_SIZE = 84;
public const float CATCHER_SIZE = 100;
protected readonly Catcher MovableCatcher;
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI
private DrawableCatchHitObject lastPlateableFruit;
public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement)
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
{
void runAfterLoaded(Action action)
{
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.UI
lastPlateableFruit.OnLoadComplete = _ => action();
}
if (judgement.IsHit && fruit.CanBePlated)
if (result.IsHit && fruit.CanBePlated)
{
var caughtFruit = (DrawableCatchHitObject)GetVisualRepresentation?.Invoke(fruit.HitObject);
@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (fruit.HitObject.LastInCombo)
{
if (((CatchJudgement)judgement).ShouldExplode)
if (((CatchJudgement)result.Judgement).ShouldExplodeFor(result))
runAfterLoaded(() => MovableCatcher.Explode());
else
MovableCatcher.Drop();
@@ -107,6 +107,11 @@ namespace osu.Game.Rulesets.Catch.UI
public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj);
public static float GetCatcherSize(BeatmapDifficulty difficulty)
{
return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
}
public class Catcher : Container, IKeyBindingHandler<CatchAction>
{
/// <summary>
@@ -407,9 +412,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
public void Explode()
{
var fruit = caughtFruit.ToArray();
foreach (var f in fruit)
foreach (var f in caughtFruit.ToArray())
Explode(f);
}
@@ -422,15 +425,15 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.Anchor = Anchor.TopLeft;
fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
caughtFruit.Remove(fruit);
if (!caughtFruit.Remove(fruit))
// we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
return;
ExplodingFruitTarget.Add(fruit);
}
fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(fruit.Y + 50, 500, Easing.InSine);
fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine).Then().MoveToY(fruit.Y + 50, 500, Easing.InSine);
fruit.MoveToX(fruit.X + originalX * 6, 1000);
fruit.FadeOut(750);
@@ -0,0 +1,42 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class PlayfieldAdjustmentContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public PlayfieldAdjustmentContainer()
{
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3,
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
};
}
/// <summary>
/// A <see cref="Container"/> which scales its content relative to a target width.
/// </summary>
private class ScalingContainer : Container
{
protected override void Update()
{
base.Update();
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
}
}
@@ -14,24 +14,20 @@ namespace osu.Game.Rulesets.Mania.Tests
/// </summary>
public class ScrollingTestContainer : Container
{
private readonly ScrollingDirection direction;
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
public ScrollingTestContainer(ScrollingDirection direction)
{
this.direction = direction;
scrollingInfo.Direction.Value = direction;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<IScrollingInfo>(new ScrollingInfo { Direction = { Value = direction }});
return dependencies;
}
public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up;
}
private class ScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
}
public class TestScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
}
}
@@ -0,0 +1,32 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestCaseEditor : EditorTestCase
{
private readonly Bindable<ManiaScrollingDirection> direction = new Bindable<ManiaScrollingDirection>();
public TestCaseEditor()
: base(new ManiaRuleset())
{
AddStep("upwards scroll", () => direction.Value = ManiaScrollingDirection.Up);
AddStep("downwards scroll", () => direction.Value = ManiaScrollingDirection.Down);
}
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
{
var config = (ManiaConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
config.BindWith(ManiaSetting.ScrollDirection, direction);
}
}
}
+22 -11
View File
@@ -46,15 +46,20 @@ namespace osu.Game.Rulesets.Mania.Tests
Spacing = new Vector2(20),
Children = new[]
{
createNoteDisplay(ScrollingDirection.Down),
createNoteDisplay(ScrollingDirection.Up),
createHoldNoteDisplay(ScrollingDirection.Down),
createHoldNoteDisplay(ScrollingDirection.Up),
createNoteDisplay(ScrollingDirection.Down, 1, out var note1),
createNoteDisplay(ScrollingDirection.Up, 2, out var note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2),
}
};
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));
AddAssert("hold note 1 facing downwards", () => verifyAnchors(holdNote1, Anchor.y2));
AddAssert("hold note 2 facing upwards", () => verifyAnchors(holdNote2, Anchor.y0));
}
private Drawable createNoteDisplay(ScrollingDirection direction)
private Drawable createNoteDisplay(ScrollingDirection direction, int identifier, out DrawableNote hitObject)
{
var note = new Note { StartTime = 999999999 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@@ -62,24 +67,24 @@ namespace osu.Game.Rulesets.Mania.Tests
return new ScrollingTestContainer(direction)
{
AutoSizeAxes = Axes.Both,
Child = new NoteContainer(direction, $"note, scrolling {direction.ToString().ToLowerInvariant()}")
Child = new NoteContainer(direction, $"note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}")
{
Child = new DrawableNote(note) { AccentColour = Color4.OrangeRed }
Child = hitObject = new DrawableNote(note) { AccentColour = Color4.OrangeRed }
}
};
}
private Drawable createHoldNoteDisplay(ScrollingDirection direction)
private Drawable createHoldNoteDisplay(ScrollingDirection direction, int identifier, out DrawableHoldNote hitObject)
{
var note = new HoldNote { StartTime = 999999999, Duration = 1000 };
var note = new HoldNote { StartTime = 999999999, Duration = 5000 };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return new ScrollingTestContainer(direction)
{
AutoSizeAxes = Axes.Both,
Child = new NoteContainer(direction, $"hold note, scrolling {direction.ToString().ToLowerInvariant()}")
Child = new NoteContainer(direction, $"hold note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}")
{
Child = new DrawableHoldNote(note)
Child = hitObject = new DrawableHoldNote(note)
{
RelativeSizeAxes = Axes.Both,
AccentColour = Color4.OrangeRed,
@@ -88,6 +93,12 @@ namespace osu.Game.Rulesets.Mania.Tests
};
}
private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor)
=> hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor);
private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor)
=> verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor));
private class NoteContainer : Container
{
private readonly Container content;
+19 -2
View File
@@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.Tests
private readonly List<ManiaStage> stages = new List<ManiaStage>();
private FillFlowContainer<ScrollingTestContainer> fill;
public TestCaseStage()
: base(columns)
{
@@ -32,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
Child = fill = new FillFlowContainer<ScrollingTestContainer>
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@@ -54,8 +57,22 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("hold note", createHoldNote);
AddStep("minor bar line", () => createBarLine(false));
AddStep("major bar line", () => createBarLine(true));
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.TopCentre));
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.BottomCentre));
AddStep("flip direction", () =>
{
foreach (var c in fill.Children)
c.Flip();
});
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.BottomCentre));
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.TopCentre));
}
private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
private void createNote()
{
foreach (var stage in stages)
@@ -101,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}
}
private Drawable createStage(ScrollingDirection direction, ManiaAction action)
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
{
var specialAction = ManiaAction.Special1;
@@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
TargetColumns = (int)Math.Max(1, roundedCircleSize);
else
{
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count();
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count;
if (percentSliderOrSpinner < 0.2)
TargetColumns = 7;
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
@@ -173,19 +173,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects;
int nextColumn = Random.Next(RandomStart, TotalColumns);
int nextColumn = GetRandomColumn();
for (int i = 0; i < Math.Min(usableColumns, noteCount); i++)
{
while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column
nextColumn = Random.Next(RandomStart, TotalColumns);
// Find available column
nextColumn = FindAvailableColumn(nextColumn, pattern, PreviousPattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
}
// This is can't be combined with the above loop due to RNG
for (int i = 0; i < noteCount - usableColumns; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, TotalColumns);
nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
}
@@ -210,18 +209,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
{
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, TotalColumns);
}
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
int lastColumn = nextColumn;
for (int i = 0; i < noteCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
while (nextColumn == lastColumn)
nextColumn = Random.Next(RandomStart, TotalColumns);
nextColumn = FindAvailableColumn(nextColumn, validation: c => c != lastColumn);
lastColumn = nextColumn;
startTime += SegmentDuration;
}
@@ -313,7 +307,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns > 2)
addToPattern(pattern, nextColumn, startTime, startTime);
nextColumn = Random.Next(RandomStart, TotalColumns);
nextColumn = GetRandomColumn();
startTime += SegmentDuration;
}
@@ -392,16 +386,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
{
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, TotalColumns);
}
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
for (int i = 0; i < columnRepeat; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, TotalColumns);
nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
startTime += SegmentDuration;
}
@@ -426,15 +415,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
{
while (PreviousPattern.ColumnHasObject(holdColumn))
holdColumn = Random.Next(RandomStart, TotalColumns);
}
holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
// Create the hold note
addToPattern(pattern, holdColumn, startTime, EndTime);
int nextColumn = Random.Next(RandomStart, TotalColumns);
int nextColumn = GetRandomColumn();
int noteCount;
if (ConversionDifficulty > 6.5)
noteCount = GetRandomNoteCount(0.63, 0);
@@ -455,8 +441,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
for (int j = 0; j < noteCount; j++)
{
while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn)
nextColumn = Random.Next(RandomStart, TotalColumns);
nextColumn = FindAvailableColumn(nextColumn, validation: c => c != holdColumn, patterns: rowPattern);
addToPattern(rowPattern, nextColumn, startTime, startTime);
}
}
@@ -39,32 +39,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
addToPattern(pattern, 0, generateHold);
break;
case 8:
addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold);
addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
break;
default:
if (TotalColumns > 0)
addToPattern(pattern, getNextRandomColumn(0), generateHold);
addToPattern(pattern, GetRandomColumn(), generateHold);
break;
}
return pattern;
}
/// <summary>
/// Picks a random column after a column.
/// </summary>
/// <param name="start">The starting column.</param>
/// <returns>A random column after <paramref name="start"/>.</returns>
private int getNextRandomColumn(int start)
{
int nextColumn = Random.Next(start, TotalColumns);
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(start, TotalColumns);
return nextColumn;
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>
@@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
PatternType lastStair, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime));
if (density < 0) throw new ArgumentOutOfRangeException(nameof(density));
StairType = lastStair;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
@@ -234,22 +231,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i < noteCount; i++)
{
while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking)
{
if (convertType.HasFlag(PatternType.Gathered))
{
nextColumn++;
if (nextColumn == TotalColumns)
nextColumn = RandomStart;
}
else
nextColumn = Random.Next(RandomStart, TotalColumns);
}
nextColumn = allowStacking
? FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: pattern)
: FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: new[] { pattern, PreviousPattern });
addToPattern(pattern, nextColumn);
}
return pattern;
int getNextColumn(int last)
{
if (convertType.HasFlag(PatternType.Gathered))
{
last++;
if (last == TotalColumns)
last = RandomStart;
}
else
last = GetRandomColumn();
return last;
}
}
/// <summary>
@@ -286,17 +288,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{
if (convertType.HasFlag(PatternType.ForceNotStack))
return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
var pattern = new Pattern();
bool addToCentre;
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
int nextColumn = Random.Next(RandomStart, columnLimit);
int nextColumn = GetRandomColumn(upperBound: columnLimit);
for (int i = 0; i < noteCount; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, columnLimit);
nextColumn = FindAvailableColumn(nextColumn, upperBound: columnLimit, patterns: pattern);
// Add normal note
addToPattern(pattern, nextColumn);
@@ -368,9 +372,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
addToCentre = false;
if (convertType.HasFlag(PatternType.ForceNotStack))
return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
switch (TotalColumns)
{
case 2:
@@ -3,6 +3,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
@@ -90,6 +91,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
private double? conversionDifficulty;
/// <summary>
/// A difficulty factor used for various conversion methods from osu!stable.
/// </summary>
@@ -110,11 +112,88 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;
}
}
/// <summary>
/// Finds a new column in which a <see cref="HitObject"/> can be placed.
/// This uses <see cref="GetRandomColumn"/> to pick the next candidate column.
/// </summary>
/// <param name="initialColumn">The initial column to test. This may be returned if it is already a valid column.</param>
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
/// <returns>A column for which there are no <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns>
/// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception>
protected int FindAvailableColumn(int initialColumn, params Pattern[] patterns)
=> FindAvailableColumn(initialColumn, null, patterns: patterns);
/// <summary>
/// Finds a new column in which a <see cref="HitObject"/> can be placed.
/// </summary>
/// <param name="initialColumn">The initial column to test. This may be returned if it is already a valid column.</param>
/// <param name="nextColumn">A function to retrieve the next column. If null, a randomisation scheme will be used.</param>
/// <param name="validation">A function to perform additional validation checks to determine if a column is a valid candidate for a <see cref="HitObject"/>.</param>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
/// <returns>A column which has passed the <paramref name="validation"/> check and for which there are no
/// <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns>
/// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception>
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int> nextColumn = null, [InstantHandle] Func<int, bool> validation = null,
params Pattern[] patterns)
{
lowerBound = lowerBound ?? RandomStart;
upperBound = upperBound ?? TotalColumns;
nextColumn = nextColumn ?? (_ => GetRandomColumn(lowerBound, upperBound));
// Check for the initial column
if (isValid(initialColumn))
return initialColumn;
// Ensure that we have at least one free column, so that an endless loop is avoided
bool hasValidColumns = false;
for (int i = lowerBound.Value; i < upperBound.Value; i++)
{
hasValidColumns = isValid(i);
if (hasValidColumns)
break;
}
if (!hasValidColumns)
throw new NotEnoughColumnsException();
// Iterate until a valid column is found. This is a random iteration in the default case.
do
{
initialColumn = nextColumn(initialColumn);
} while (!isValid(initialColumn));
return initialColumn;
bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column));
}
/// <summary>
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
/// <summary>
/// Occurs when mania conversion is stuck in an infinite loop unable to find columns to place new hitobjects in.
/// </summary>
public class NotEnoughColumnsException : Exception
{
public NotEnoughColumnsException()
: base("There were not enough columns to complete conversion.")
{
}
}
}
}
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
Set(ManiaSetting.ScrollTime, 1500.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollTime, 2250.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollDirection, ManiaScrollingDirection.Down);
}
@@ -0,0 +1,17 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaEditPlayfield : ManiaPlayfield
{
public ManiaEditPlayfield(List<StageDefinition> stages)
: base(stages)
{
}
}
}
@@ -0,0 +1,26 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using OpenTK;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaEditRulesetContainer : ManiaRulesetContainer
{
public ManiaEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
protected override Playfield CreatePlayfield() => new ManiaEditPlayfield(Beatmap.Stages)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = Vector2.One
};
}
}
@@ -0,0 +1,54 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Edit.Masks;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>
{
protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config;
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<IScrollingInfo>(new ManiaScrollingInfo(Config));
return dependencies;
}
protected override RulesetContainer<ManiaHitObject> CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
=> new ManiaEditRulesetContainer(ruleset, beatmap);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => Array.Empty<HitObjectCompositionTool>();
public override SelectionMask CreateMaskFor(DrawableHitObject hitObject)
{
switch (hitObject)
{
case DrawableNote note:
return new NoteSelectionMask(note);
case DrawableHoldNote holdNote:
return new HoldNoteSelectionMask(holdNote);
}
return base.CreateMaskFor(hitObject);
}
}
}
@@ -0,0 +1,84 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.Edit.Masks
{
public class HoldNoteSelectionMask : SelectionMask
{
public new DrawableHoldNote HitObject => (DrawableHoldNote)base.HitObject;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly BodyPiece body;
public HoldNoteSelectionMask(DrawableHoldNote hold)
: base(hold)
{
InternalChildren = new Drawable[]
{
new HoldNoteNoteSelectionMask(hold.Head),
new HoldNoteNoteSelectionMask(hold.Tail),
body = new BodyPiece
{
AccentColour = Color4.Transparent
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, IScrollingInfo scrollingInfo)
{
body.BorderColour = colours.Yellow;
direction.BindTo(scrollingInfo.Direction);
}
protected override void Update()
{
base.Update();
Size = HitObject.DrawSize + new Vector2(0, HitObject.Tail.DrawHeight);
Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft);
// 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 -= HitObject.Tail.DrawHeight;
}
private class HoldNoteNoteSelectionMask : NoteSelectionMask
{
public HoldNoteNoteSelectionMask(DrawableNote note)
: base(note)
{
Select();
}
protected override void Update()
{
base.Update();
Anchor = HitObject.Anchor;
Origin = HitObject.Origin;
Position = HitObject.DrawPosition;
}
// Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input.
public override bool HandlePositionalInput => false;
}
}
}
@@ -0,0 +1,39 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Mania.Edit.Masks
{
public class NoteSelectionMask : SelectionMask
{
public NoteSelectionMask(DrawableNote note)
: base(note)
{
Scale = note.Scale;
CornerRadius = 5;
Masking = true;
AddInternal(new NotePiece());
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Yellow;
}
protected override void Update()
{
base.Update();
Size = HitObject.DrawSize;
Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft);
}
}
}
@@ -1,27 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTailJudgement : ManiaJudgement
{
/// <summary>
/// Whether the hold note has been released too early and shouldn't give full score for the release.
/// </summary>
public bool HasBroken;
protected override int NumericResultFor(HitResult result)
{
switch (result)
{
default:
return base.NumericResultFor(result);
case HitResult.Great:
case HitResult.Perfect:
return base.NumericResultFor(HasBroken ? HitResult.Good : result);
}
}
}
}
+4
View File
@@ -19,9 +19,11 @@ using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania
@@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score);
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
@@ -5,13 +5,11 @@ using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToRulesetContainer<ManiaHitObject>
public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToBeatmap<ManiaHitObject>
{
public override string Name => "Dual Stages";
public override string ShortenedName => "DS";
@@ -34,22 +32,21 @@ namespace osu.Game.Rulesets.Mania.Mods
mbc.TargetColumns *= 2;
}
public void ApplyToRulesetContainer(RulesetContainer<ManiaHitObject> rulesetContainer)
public void ApplyToBeatmap(Beatmap<ManiaHitObject> beatmap)
{
var mrc = (ManiaRulesetContainer)rulesetContainer;
// Although this can work, for now let's not allow keymods for mania-specific beatmaps
if (isForCurrentRuleset)
return;
var maniaBeatmap = (ManiaBeatmap)beatmap;
var newDefinitions = new List<StageDefinition>();
foreach (var existing in mrc.Beatmap.Stages)
foreach (var existing in maniaBeatmap.Stages)
{
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
}
mrc.Beatmap.Stages = newDefinitions;
maniaBeatmap.Stages = newDefinitions;
}
public PlayfieldType PlayfieldType => PlayfieldType.Dual;
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using OpenTK.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
@@ -19,10 +18,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public class DrawableHoldNote : DrawableManiaHitObject<HoldNote>, IKeyBindingHandler<ManiaAction>
{
public override bool DisplayJudgement => false;
public override bool DisplayResult => false;
private readonly DrawableNote head;
private readonly DrawableNote tail;
public readonly DrawableNote Head;
public readonly DrawableNote Tail;
private readonly BodyPiece bodyPiece;
@@ -57,12 +56,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
HoldStartTime = () => holdStartTime
})
},
head = new DrawableHeadNote(this)
Head = new DrawableHeadNote(this)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
tail = new DrawableTailNote(this)
Tail = new DrawableTailNote(this)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
@@ -72,8 +71,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
foreach (var tick in tickContainer)
AddNested(tick);
AddNested(head);
AddNested(tail);
AddNested(Head);
AddNested(Tail);
}
protected override void OnDirectionChanged(ScrollingDirection direction)
@@ -91,16 +90,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
base.AccentColour = value;
bodyPiece.AccentColour = value;
head.AccentColour = value;
tail.AccentColour = value;
Head.AccentColour = value;
Tail.AccentColour = value;
tickContainer.ForEach(t => t.AccentColour = value);
}
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (tail.AllJudged)
AddJudgement(new HoldNoteJudgement { Result = HitResult.Perfect });
if (Tail.AllJudged)
ApplyResult(r => r.Type = HitResult.Perfect);
}
protected override void Update()
@@ -108,8 +107,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
base.Update();
// Make the body piece not lie under the head note
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * head.Height / 2;
bodyPiece.Height = DrawHeight - head.Height / 2 + tail.Height / 2;
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
}
protected void BeginHold()
{
holdStartTime = Time.Current;
bodyPiece.Hitting = true;
}
protected void EndHold()
{
holdStartTime = null;
bodyPiece.Hitting = false;
}
public bool OnPressed(ManiaAction action)
@@ -124,8 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// The user has pressed during the body of the hold note, after the head note and its hit windows have passed
// and within the limited range of the above if-statement. This state will be managed by the head note if the
// user has pressed during the hit windows of the head note.
holdStartTime = Time.Current;
BeginHold();
return true;
}
@@ -138,10 +148,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
holdStartTime = null;
EndHold();
// If the key has been released too early, the user should not receive full score for the release
if (!tail.IsHit)
if (!Tail.IsHit)
hasBroken = true;
return true;
@@ -166,12 +176,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return false;
// If the key has been released too early, the user should not receive full score for the release
if (Judgements.Any(j => j.Result == HitResult.Miss))
if (Result.Type == HitResult.Miss)
holdNote.hasBroken = true;
// The head note also handles early hits before the body, but we want accurate early hits to count as the body being held
// The body doesn't handle these early early hits, so we have to explicitly set the holding state here
holdNote.holdStartTime = Time.Current;
holdNote.BeginHold();
return true;
}
@@ -197,7 +207,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
this.holdNote = holdNote;
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// Factor in the release lenience
timeOffset /= release_window_lenience;
@@ -205,13 +215,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
{
AddJudgement(new HoldNoteTailJudgement
{
Result = HitResult.Miss,
HasBroken = holdNote.hasBroken
});
}
ApplyResult(r => r.Type = HitResult.Miss);
return;
}
@@ -220,10 +224,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (result == HitResult.None)
return;
AddJudgement(new HoldNoteTailJudgement
ApplyResult(r =>
{
Result = result,
HasBroken = holdNote.hasBroken
if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect))
result = HitResult.Good;
r.Type = result;
});
}
@@ -238,7 +244,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
UpdateJudgement(true);
UpdateResult(true);
// Handled by the hold note, which will set holding = false
return false;
@@ -7,7 +7,6 @@ using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Scoring;
@@ -72,29 +71,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!userTriggered)
return;
if (Time.Current < HitObject.StartTime)
return;
if (HoldStartTime?.Invoke() > HitObject.StartTime)
return;
var startTime = HoldStartTime?.Invoke();
AddJudgement(new HoldNoteTickJudgement { Result = HitResult.Perfect });
}
protected override void Update()
{
if (AllJudged)
return;
if (HoldStartTime?.Invoke() == null)
return;
UpdateJudgement(true);
if (startTime == null || startTime > HitObject.StartTime)
ApplyResult(r => r.Type = HitResult.Miss);
else
ApplyResult(r => r.Type = HitResult.Perfect);
}
}
}
@@ -6,7 +6,6 @@ using OpenTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
@@ -56,12 +55,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
AddJudgement(new ManiaJudgement { Result = HitResult.Miss });
ApplyResult(r => r.Type = HitResult.Miss);
return;
}
@@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (result == HitResult.None)
return;
AddJudgement(new ManiaJudgement { Result = result });
ApplyResult(r => r.Type = result);
}
public virtual bool OnPressed(ManiaAction action)
@@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
return UpdateJudgement(true);
return UpdateResult(true);
}
public virtual bool OnReleased(ManiaAction action) => false;
@@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
background = new Box { RelativeSizeAxes = Axes.Both },
foreground = new BufferedContainer
{
Blending = BlendingMode.Additive,
RelativeSizeAxes = Axes.Both,
CacheDrawnFrameBuffer = true,
Children = new Drawable[]
@@ -73,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
private Color4 accentColour;
public Color4 AccentColour
{
get { return accentColour; }
@@ -86,6 +88,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
public bool Hitting
{
get { return hitting; }
set
{
hitting = value;
updateAccentColour();
}
}
private Cached subtractionCache = new Cached();
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
@@ -118,13 +130,26 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
private bool hitting;
private void updateAccentColour()
{
if (!IsLoaded)
return;
foreground.Colour = AccentColour.Opacity(0.9f);
background.Colour = AccentColour.Opacity(0.6f);
foreground.Colour = AccentColour.Opacity(0.5f);
background.Colour = AccentColour.Opacity(0.7f);
const float animation_length = 50;
foreground.ClearTransforms(false, nameof(foreground.Colour));
if (hitting)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (foreground.BeginDelayedSequence(synchronisedOffset))
foreground.FadeColour(AccentColour.Lighten(0.2f), animation_length).Then().FadeColour(foreground.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}
+8 -4
View File
@@ -3,6 +3,8 @@
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Mania.Objects
@@ -55,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary>
/// The tail note of the hold.
/// </summary>
public readonly Note Tail = new Note();
public readonly TailNote Tail = new TailNote();
/// <summary>
/// The time between ticks of this hold.
@@ -68,9 +70,6 @@ namespace osu.Game.Rulesets.Mania.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
Head.ApplyDefaults(controlPointInfo, difficulty);
Tail.ApplyDefaults(controlPointInfo, difficulty);
}
protected override void CreateNestedHitObjects()
@@ -78,6 +77,9 @@ namespace osu.Game.Rulesets.Mania.Objects
base.CreateNestedHitObjects();
createTicks();
AddNested(Head);
AddNested(Tail);
}
private void createTicks()
@@ -94,5 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects
});
}
}
public override Judgement CreateJudgement() => new HoldNoteJudgement();
}
}
@@ -1,6 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
@@ -8,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
public class HoldNoteTick : ManiaHitObject
{
public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
}
}
+4
View File
@@ -1,6 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
@@ -8,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
public class Note : ManiaHitObject
{
public override Judgement CreateJudgement() => new ManiaJudgement();
}
}
@@ -0,0 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
namespace osu.Game.Rulesets.Mania.Objects
{
public class TailNote : Note
{
public override Judgement CreateJudgement() => new ManiaJudgement();
}
}
@@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
@@ -97,31 +96,20 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
}
protected override void SimulateAutoplay(Beatmap<ManiaHitObject> beatmap)
protected override void ApplyBeatmap(Beatmap<ManiaHitObject> beatmap)
{
base.ApplyBeatmap(beatmap);
BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty;
hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max);
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max);
}
protected override void SimulateAutoplay(Beatmap<ManiaHitObject> beatmap)
{
while (true)
{
foreach (var obj in beatmap.HitObjects)
{
var holdNote = obj as HoldNote;
if (holdNote != null)
{
// Head
AddJudgement(new ManiaJudgement { Result = HitResult.Perfect });
// Ticks
int tickCount = holdNote.NestedHitObjects.OfType<HoldNoteTick>().Count();
for (int i = 0; i < tickCount; i++)
AddJudgement(new HoldNoteTickJudgement { Result = HitResult.Perfect });
}
AddJudgement(new ManiaJudgement { Result = HitResult.Perfect });
}
base.SimulateAutoplay(beatmap);
if (!HasFailed)
break;
@@ -133,20 +121,20 @@ namespace osu.Game.Rulesets.Mania.Scoring
}
}
protected override void OnNewJudgement(Judgement judgement)
protected override void ApplyResult(JudgementResult result)
{
base.OnNewJudgement(judgement);
base.ApplyResult(result);
bool isTick = judgement is HoldNoteTickJudgement;
bool isTick = result.Judgement is HoldNoteTickJudgement;
if (isTick)
{
if (judgement.IsHit)
if (result.IsHit)
Health.Value += hpMultiplier * hp_increase_tick;
}
else
{
switch (judgement.Result)
switch (result.Type)
{
case HitResult.Miss:
Health.Value += hpMissMultiplier * hp_increase_miss;
+7 -6
View File
@@ -30,8 +30,6 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly Container explosionContainer;
protected override Container<Drawable> Content => hitObjectArea;
public Column()
{
RelativeSizeAxes = Axes.Y;
@@ -54,7 +52,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hitObjectArea = new ColumnHitObjectArea { RelativeSizeAxes = Axes.Both },
hitObjectArea = new ColumnHitObjectArea(HitObjectContainer)
{
RelativeSizeAxes = Axes.Both,
},
explosionContainer = new Container
{
Name = "Hit explosions",
@@ -131,14 +132,14 @@ namespace osu.Game.Rulesets.Mania.UI
public override void Add(DrawableHitObject hitObject)
{
hitObject.AccentColour = AccentColour;
hitObject.OnJudgement += OnJudgement;
hitObject.OnNewResult += OnNewResult;
HitObjects.Add(hitObject);
}
internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
if (!judgement.IsHit || !judgedObject.DisplayJudgement || !DisplayJudgements)
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements)
return;
explosionContainer.Add(new HitExplosion(judgedObject)
@@ -8,28 +8,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public class ColumnHitObjectArea : Container, IHasAccentColour
public class ColumnHitObjectArea : CompositeDrawable, IHasAccentColour
{
private const float hit_target_height = 10;
private const float hit_target_bar_height = 2;
private Container<Drawable> content;
protected override Container<Drawable> Content => content;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Container hitTargetLine;
private readonly Container hitTargetLine;
private readonly Drawable hitTargetBar;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
{
Drawable hitTargetBar;
InternalChildren = new[]
{
hitTargetBar = new Box
@@ -45,13 +41,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
content = new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
},
hitObjectContainer
};
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction =>
{
@@ -8,10 +8,10 @@ using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.UI
{
internal class DrawableManiaJudgement : DrawableJudgement
public class DrawableManiaJudgement : DrawableJudgement
{
public DrawableManiaJudgement(Judgement judgement, DrawableHitObject judgedObject)
: base(judgement, judgedObject)
public DrawableManiaJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject)
{
}
@@ -28,12 +28,13 @@ namespace osu.Game.Rulesets.Mania.UI
this.FadeInFromZero(50, Easing.OutQuint);
if (Judgement.IsHit)
if (Result.IsHit)
{
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50).FadeOut(200).ScaleTo(0.75f, 250);
JudgementBody.Delay(50).ScaleTo(0.75f, 250);
this.Delay(50).FadeOut(200);
}
Expire();
+8 -7
View File
@@ -1,22 +1,21 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects;
using osu.Framework.Graphics.Containers;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaPlayfield : ManiaScrollingPlayfield
{
public List<Column> Columns => stages.SelectMany(x => x.Columns).ToList();
private readonly List<ManiaStage> stages = new List<ManiaStage>();
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
@@ -27,12 +26,14 @@ namespace osu.Game.Rulesets.Mania.UI
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");
Size = new Vector2(1, 0.8f);
GridContainer playfieldGrid;
InternalChild = playfieldGrid = new GridContainer
AddInternal(playfieldGrid = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] { new Drawable[stageDefinitions.Count] }
};
});
var normalColumnAction = ManiaAction.Key1;
var specialColumnAction = ManiaAction.Special1;
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
@@ -25,7 +24,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -35,8 +33,7 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private ScrollingInfo scrollingInfo;
protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config;
public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -73,9 +70,6 @@ namespace osu.Game.Rulesets.Mania.UI
private void load()
{
BarLines.ForEach(Playfield.Add);
((ManiaConfigManager)Config).BindWith(ManiaSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(d => scrollingInfo.Direction.Value = (ScrollingDirection)d, true);
}
private DependencyContainer dependencies;
@@ -83,11 +77,14 @@ namespace osu.Game.Rulesets.Mania.UI
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<IScrollingInfo>(scrollingInfo = new ScrollingInfo());
if (dependencies.Get<ManiaScrollingInfo>() == null)
dependencies.CacheAs<IScrollingInfo>(new ManiaScrollingInfo(Config));
return dependencies;
}
protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages)
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -99,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
protected override DrawableHitObject<ManiaHitObject> GetVisualRepresentation(ManiaHitObject h)
public override DrawableHitObject<ManiaHitObject> GetVisualRepresentation(ManiaHitObject h)
{
switch (h)
{
@@ -112,14 +109,6 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
private class ScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
}
}
}
@@ -0,0 +1,23 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaScrollingInfo : IScrollingInfo
{
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
public ManiaScrollingInfo(ManiaConfigManager config)
{
config.BindWith(ManiaSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true);
}
}
}
@@ -7,7 +7,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaScrollingPlayfield : ScrollingPlayfield
public abstract class ManiaScrollingPlayfield : ScrollingPlayfield
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
+7 -7
View File
@@ -23,15 +23,14 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary>
/// A collection of <see cref="Column"/>s.
/// </summary>
internal class ManiaStage : ManiaScrollingPlayfield
public class ManiaStage : ManiaScrollingPlayfield
{
public const float HIT_TARGET_POSITION = 50;
public IReadOnlyList<Column> Columns => columnFlow.Children;
private readonly FillFlowContainer<Column> columnFlow;
protected override Container<Drawable> Content => barLineContainer;
private readonly Container<Drawable> barLineContainer;
private readonly Container barLineContainer;
public Container<DrawableManiaJudgement> Judgements => judgements;
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
@@ -105,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.UI
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Child = HitObjectContainer
}
},
judgements = new JudgementContainer<DrawableManiaJudgement>
@@ -156,18 +156,18 @@ namespace osu.Game.Rulesets.Mania.UI
var maniaObject = (ManiaHitObject)h.HitObject;
int columnIndex = maniaObject.Column - firstColumnIndex;
Columns.ElementAt(columnIndex).Add(h);
h.OnJudgement += OnJudgement;
h.OnNewResult += OnNewResult;
}
public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline));
internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
if (!judgedObject.DisplayJudgement || !DisplayJudgements)
if (!judgedObject.DisplayResult || !DisplayJudgements)
return;
judgements.Clear();
judgements.Add(new DrawableManiaJudgement(judgement, judgedObject)
judgements.Add(new DrawableManiaJudgement(result, judgedObject)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -0,0 +1,64 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class StackingTest
{
[Test]
public void TestStacking()
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data)))
using (var reader = new StreamReader(stream))
{
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
var objects = converted.HitObjects.ToList();
// The last hitobject triggers the stacking
for (int i = 0; i < objects.Count - 1; i++)
Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
}
}
private const string beatmap_data = @"
osu file format v14
[General]
StackLeniency: 0.2
[Difficulty]
ApproachRate:9.2
SliderMultiplier:1
SliderTickRate:0.5
[TimingPoints]
217871,6400,4,2,1,20,1,0
217871,-800,4,2,1,20,0,0
218071,-787.5,4,2,1,20,0,0
218271,-775,4,2,1,20,0,0
218471,-762.5,4,2,1,20,0,0
218671,-750,4,2,1,20,0,0
240271,-10,4,2,0,5,0,0
[HitObjects]
311,185,217871,6,0,L|318:158,1,25
311,185,218071,2,0,L|335:170,1,25
311,185,218271,2,0,L|338:192,1,25
311,185,218471,2,0,L|325:209,1,25
311,185,218671,2,0,L|304:212,1,25
311,185,240271,5,0,0:0:0:0:
";
}
}
@@ -5,12 +5,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using OpenTK;
using osu.Game.Rulesets.Osu.Judgements;
using System.Collections.Generic;
using System;
using osu.Game.Rulesets.Mods;
@@ -87,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
}
private class TestDrawableHitCircle : DrawableHitCircle
protected class TestDrawableHitCircle : DrawableHitCircle
{
private readonly bool auto;
@@ -96,19 +94,17 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto;
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
public void TriggerJudgement() => UpdateResult(true);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (auto && !userTriggered && timeOffset > 0)
{
// force success
AddJudgement(new OsuJudgement
{
Result = HitResult.Great
});
State.Value = ArmedState.Hit;
ApplyResult(r => r.Type = HitResult.Great);
}
else
base.CheckForJudgements(userTriggered, timeOffset);
base.CheckForResult(userTriggered, timeOffset);
}
}
}
@@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseHitCirclePlacementMask : HitObjectPlacementMaskTestCase
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
protected override PlacementMask CreateMask() => new HitCirclePlacementMask();
}
}
@@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseHitCircleSelectionMask : HitObjectSelectionMaskTestCase
{
private readonly DrawableHitCircle drawableObject;
public TestCaseHitCircleSelectionMask()
{
var hitCircle = new HitCircle { Position = new Vector2(256, 192) };
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(drawableObject = new DrawableHitCircle(hitCircle));
}
protected override SelectionMask CreateMask() => new HitCircleSelectionMask(drawableObject);
}
}
@@ -0,0 +1,23 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseShaking : TestCaseHitCircle
{
public override void Add(Drawable drawable)
{
base.Add(drawable);
if (drawable is TestDrawableHitCircle hitObject)
{
Scheduler.AddDelayed(() => hitObject.TriggerJudgement(),
hitObject.HitObject.StartTime - (hitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current);
}
}
}
}
+15 -15
View File
@@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(239, 176),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(154, 28),
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-(distance / 2), 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(distance, 0),
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(200, 200),
@@ -181,10 +181,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
CurveType = CurveType.Linear,
PathType = PathType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(150, 75),
@@ -207,10 +207,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
CurveType = CurveType.Bezier,
PathType = PathType.Bezier,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(150, 75),
@@ -232,10 +232,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
CurveType = CurveType.Linear,
PathType = PathType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(0, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(-200, 0),
@@ -264,8 +264,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-100, 0),
CurveType = CurveType.Catmull,
ControlPoints = new List<Vector2>
PathType = PathType.Catmull,
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(50, -50),
@@ -304,13 +304,13 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
drawable.OnJudgement += onJudgement;
drawable.OnNewResult += onNewResult;
Add(drawable);
}
private float judgementOffsetDirection = 1;
private void onJudgement(DrawableHitObject judgedObject, Judgement judgement)
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
var osuObject = judgedObject as DrawableOsuHitObject;
if (osuObject == null)
@@ -321,8 +321,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = judgement.IsHit ? "Hit!" : "Miss!",
Colour = judgement.IsHit ? Color4.Green : Color4.Red,
Text = result.IsHit ? "Hit!" : "Miss!",
Colour = result.IsHit ? Color4.Green : Color4.Red,
TextSize = 30,
Position = osuObject.HitObject.StackedEndPosition + judgementOffsetDirection * new Vector2(0, 45)
});
@@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseSliderPlacementMask : HitObjectPlacementMaskTestCase
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
protected override PlacementMask CreateMask() => new SliderPlacementMask();
}
}
@@ -0,0 +1,55 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks;
using osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseSliderSelectionMask : HitObjectSelectionMaskTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SliderSelectionMask),
typeof(SliderCircleSelectionMask),
typeof(SliderBodyPiece),
typeof(SliderCircle),
typeof(PathControlPointVisualiser),
typeof(PathControlPointPiece)
};
private readonly DrawableSlider drawableObject;
public TestCaseSliderSelectionMask()
{
var slider = new Slider
{
Position = new Vector2(256, 192),
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(150, 150),
new Vector2(300, 0)
},
PathType = PathType.Bezier,
Distance = 350
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(drawableObject = new DrawableSlider(slider));
}
protected override SelectionMask CreateMask() => new SliderSelectionMask(drawableObject);
}
}
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto;
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1)
{
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Tests
auto = false;
}
base.CheckForJudgements(userTriggered, timeOffset);
base.CheckForResult(userTriggered, timeOffset);
}
}
}
@@ -0,0 +1,20 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Edit.Masks.SpinnerMasks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseSpinnerPlacementMask : HitObjectPlacementMaskTestCase
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
protected override PlacementMask CreateMask() => new SpinnerPlacementMask();
}
}
@@ -0,0 +1,50 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Masks.SpinnerMasks;
using osu.Game.Rulesets.Osu.Edit.Masks.SpinnerMasks.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseSpinnerSelectionMask : HitObjectSelectionMaskTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SpinnerSelectionMask),
typeof(SpinnerPiece)
};
private readonly DrawableSpinner drawableSpinner;
public TestCaseSpinnerSelectionMask()
{
var spinner = new Spinner
{
Position = new Vector2(256, 256),
StartTime = -1000,
EndTime = 2000
};
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
Add(new Container
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
Child = drawableSpinner = new DrawableSpinner(spinner)
});
}
protected override SelectionMask CreateMask() => new SpinnerSelectionMask(drawableSpinner) { Size = new Vector2(0.5f) };
}
}
@@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
internal class OsuBeatmapConverter : BeatmapConverter<OsuHitObject>
public class OsuBeatmapConverter : BeatmapConverter<OsuHitObject>
{
public OsuBeatmapConverter(IBeatmap beatmap)
: base(beatmap)
@@ -36,13 +36,17 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime,
Samples = original.Samples,
ControlPoints = curveData.ControlPoints,
CurveType = curveData.CurveType,
PathType = curveData.PathType,
Distance = curveData.Distance,
RepeatSamples = curveData.RepeatSamples,
RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
};
}
else if (endTimeData != null)
@@ -52,7 +56,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime,
Samples = original.Samples,
EndTime = endTimeData.EndTime,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
};
}
else
@@ -62,7 +68,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime,
Samples = original.Samples,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
};
}
}
@@ -8,27 +8,33 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
internal class OsuBeatmapProcessor : BeatmapProcessor
public class OsuBeatmapProcessor : BeatmapProcessor
{
private const int stack_distance = 3;
public OsuBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
}
public override void PreProcess()
public override void PostProcess()
{
base.PreProcess();
applyStacking((Beatmap<OsuHitObject>)Beatmap);
base.PostProcess();
var osuBeatmap = (Beatmap<OsuHitObject>)Beatmap;
// Reset stacking
foreach (var h in osuBeatmap.HitObjects)
h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(osuBeatmap);
else
applyStackingOld(osuBeatmap);
}
private void applyStacking(Beatmap<OsuHitObject> beatmap)
{
const int stack_distance = 3;
// Reset stacking
for (int i = 0; i <= beatmap.HitObjects.Count - 1; i++)
beatmap.HitObjects[i].StackHeight = 0;
// Extend the end index to include objects they are stacked on
int extendedEndIndex = beatmap.HitObjects.Count - 1;
for (int i = beatmap.HitObjects.Count - 1; i >= 0; i--)
@@ -167,5 +173,40 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
}
}
private void applyStackingOld(Beatmap<OsuHitObject> beatmap)
{
for (int i = 0; i < beatmap.HitObjects.Count; i++)
{
OsuHitObject currHitObject = beatmap.HitObjects[i];
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
continue;
double startTime = (currHitObject as IHasEndTime)?.EndTime ?? currHitObject.StartTime;
int sliderStack = 0;
for (int j = i + 1; j < beatmap.HitObjects.Count; j++)
{
double stackThreshold = beatmap.HitObjects[i].TimePreempt * beatmap.BeatmapInfo.StackLeniency;
if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime)
break;
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
{
currHitObject.StackHeight++;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
}
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.EndPosition) < stack_distance)
{
//Case for sliders - bump notes down and right, rather than up and left.
sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
}
}
}
}
}
}
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sectionLength = section_length * timeRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = 2 * sectionLength;
double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
foreach (OsuDifficultyHitObject h in difficultyBeatmap)
{
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate;
int maxCombo = beatmap.HitObjects.Count();
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
beatmapMaxCombo = Beatmap.HitObjects.Count();
beatmapMaxCombo = Beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
}
@@ -3,6 +3,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
@@ -23,8 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
difficultyObjects = createDifficultyObjectEnumerator(objects.OrderBy(h => h.StartTime).ToList(), timeRate);
}
/// <summary>
@@ -21,15 +21,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public OsuHitObject BaseObject { get; }
/// <summary>
/// Normalized distance from the <see cref="OsuHitObject.StackedPosition"/> of the previous <see cref="OsuDifficultyHitObject"/>.
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double Distance { get; private set; }
public double JumpDistance { get; private set; }
/// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double TravelDistance { get; private set; }
/// <summary>
/// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
/// </summary>
public double StrainTime { get; private set; }
private readonly OsuHitObject lastObject;
private readonly double timeRate;
@@ -51,31 +61,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances()
{
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
double scalingFactor = normalized_radius / BaseObject.Radius;
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
if (BaseObject.Radius < 30)
{
double smallCircleBonus = Math.Min(30 - BaseObject.Radius, 5) / 50;
float smallCircleBonus = Math.Min(30 - (float)BaseObject.Radius, 5) / 50;
scalingFactor *= 1 + smallCircleBonus;
}
Vector2 lastCursorPosition = lastObject.StackedPosition;
float lastTravelDistance = 0;
var lastSlider = lastObject as Slider;
if (lastSlider != null)
{
computeSliderCursorPosition(lastSlider);
lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
lastTravelDistance = lastSlider.LazyTravelDistance;
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
}
Distance = (lastTravelDistance + (BaseObject.StackedPosition - lastCursorPosition).Length) * scalingFactor;
// Don't need to jump to reach spinners
if (!(BaseObject is Spinner))
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
}
private void setTimingValues()
{
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate);
DeltaTime = (BaseObject.StartTime - lastObject.StartTime) / timeRate;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(50, DeltaTime);
}
private void computeSliderCursorPosition(Slider slider)
@@ -87,8 +101,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
float approxFollowCircleRadius = (float)(slider.Radius * 3);
var computeVertex = new Action<double>(t =>
{
double progress = ((int)t - (int)slider.StartTime) / (float)(int)slider.SpanDuration;
if (progress % 2 > 1)
progress = 1 - progress % 1;
else
progress = progress % 1;
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
var diff = slider.StackedPositionAt(t) - slider.LazyEndPosition.Value;
var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
float dist = diff.Length;
if (dist > approxFollowCircleRadius)
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.Distance, 0.99) / current.DeltaTime;
protected override double StrainValueOf(OsuDifficultyHitObject current)
=> (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.StrainTime;
}
}
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double StrainValueOf(OsuDifficultyHitObject current)
{
double distance = current.Distance;
double distance = current.TravelDistance + current.JumpDistance;
double speedValue;
if (distance > single_spacing_threshold)
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
else
speedValue = 0.95;
return speedValue / current.DeltaTime;
return speedValue / current.StrainTime;
}
}
}
@@ -0,0 +1,20 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit
{
public class HitCircleCompositionTool : HitObjectCompositionTool
{
public HitCircleCompositionTool()
: base(nameof(HitCircle))
{
}
public override PlacementMask CreatePlacementMask() => new HitCirclePlacementMask();
}
}
@@ -1,37 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
{
public class HitCircleMask : HitObjectMask
{
public HitCircleMask(DrawableHitCircle hitCircle)
: base(hitCircle)
{
Origin = Anchor.Centre;
Position = hitCircle.Position;
Size = hitCircle.Size;
Scale = hitCircle.Scale;
CornerRadius = Size.X / 2;
AddInternal(new RingPiece());
hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Yellow;
}
}
}
@@ -1,61 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
{
public class SliderCircleMask : HitObjectMask
{
public SliderCircleMask(DrawableHitCircle sliderHead, DrawableSlider slider)
: this(sliderHead, Vector2.Zero, slider)
{
}
public SliderCircleMask(DrawableSliderTail sliderTail, DrawableSlider slider)
: this(sliderTail, ((Slider)slider.HitObject).Curve.PositionAt(1), slider)
{
}
private readonly DrawableOsuHitObject hitObject;
private SliderCircleMask(DrawableOsuHitObject hitObject, Vector2 position, DrawableSlider slider)
: base(hitObject)
{
this.hitObject = hitObject;
Origin = Anchor.Centre;
Position = position;
Size = slider.HeadCircle.Size;
Scale = slider.HeadCircle.Scale;
AddInternal(new RingPiece());
Select();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Yellow;
}
protected override void Update()
{
base.Update();
RelativeAnchorPosition = hitObject.RelativeAnchorPosition;
}
// Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.
public override bool HandleMouseInput => false;
}
}
@@ -1,67 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
{
public class SliderMask : HitObjectMask
{
private readonly SliderBody body;
private readonly DrawableSlider slider;
public SliderMask(DrawableSlider slider)
: base(slider)
{
this.slider = slider;
Position = slider.Position;
var sliderObject = (Slider)slider.HitObject;
InternalChildren = new Drawable[]
{
body = new SliderBody(sliderObject)
{
AccentColour = Color4.Transparent,
PathWidth = sliderObject.Scale * 64
},
new SliderCircleMask(slider.HeadCircle, slider),
new SliderCircleMask(slider.TailCircle, slider),
};
sliderObject.PositionChanged += _ => Position = slider.Position;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
body.BorderColour = colours.Yellow;
}
protected override void Update()
{
base.Update();
Size = slider.Size;
OriginPosition = slider.OriginPosition;
// Need to cause one update
body.UpdateProgress(0);
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => body.ReceiveMouseInputAt(screenSpacePos);
public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition);
public override Quad SelectionQuad => body.PathDrawQuad;
}
}
@@ -0,0 +1,44 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks.Components
{
public class HitCirclePiece : CompositeDrawable
{
private readonly HitCircle hitCircle;
public HitCirclePiece(HitCircle hitCircle)
{
this.hitCircle = hitCircle;
Origin = Anchor.Centre;
Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2);
Scale = new Vector2(hitCircle.Scale);
CornerRadius = Size.X / 2;
InternalChild = new RingPiece();
hitCircle.PositionChanged += _ => UpdatePosition();
hitCircle.StackHeightChanged += _ => UpdatePosition();
hitCircle.ScaleChanged += _ => Scale = new Vector2(hitCircle.Scale);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Yellow;
UpdatePosition();
}
protected virtual void UpdatePosition() => Position = hitCircle.StackedPosition;
}
}
@@ -0,0 +1,42 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks.Components;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks
{
public class HitCirclePlacementMask : PlacementMask
{
public new HitCircle HitObject => (HitCircle)base.HitObject;
public HitCirclePlacementMask()
: base(new HitCircle())
{
InternalChild = new HitCirclePiece(HitObject);
}
protected override void LoadComplete()
{
base.LoadComplete();
// Fixes a 1-frame position discrpancy due to the first mouse move event happening in the next frame
HitObject.Position = GetContainingInputManager().CurrentState.Mouse.Position;
}
protected override bool OnClick(ClickEvent e)
{
HitObject.StartTime = EditorClock.CurrentTime;
EndPlacement();
return true;
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
HitObject.Position = e.MousePosition;
return true;
}
}
}
@@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Edit.Masks.HitCircleMasks
{
public class HitCircleSelectionMask : SelectionMask
{
public HitCircleSelectionMask(DrawableHitCircle hitCircle)
: base(hitCircle)
{
InternalChild = new HitCirclePiece((HitCircle)hitCircle.HitObject);
}
}
}
@@ -0,0 +1,113 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components
{
public class PathControlPointPiece : CompositeDrawable
{
private readonly Slider slider;
private readonly int index;
private readonly Path path;
private readonly CircularContainer marker;
[Resolved]
private OsuColour colours { get; set; }
public PathControlPointPiece(Slider slider, int index)
{
this.slider = slider;
this.index = index;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
path = new SmoothPath
{
Anchor = Anchor.Centre,
PathWidth = 1
},
marker = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(10),
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
}
};
}
protected override void Update()
{
base.Update();
Position = slider.StackedPosition + slider.ControlPoints[index];
marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
path.ClearVertices();
if (index != slider.ControlPoints.Length - 1)
{
path.AddVertex(Vector2.Zero);
path.AddVertex(slider.ControlPoints[index + 1] - slider.ControlPoints[index]);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnDragStart(DragStartEvent e) => true;
protected override bool OnDrag(DragEvent e)
{
var newControlPoints = slider.ControlPoints.ToArray();
if (index == 0)
{
// Special handling for the head - only the position of the slider changes
slider.Position += e.Delta;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < newControlPoints.Length; i++)
newControlPoints[i] -= e.Delta;
}
else
newControlPoints[index] += e.Delta;
if (isSegmentSeparatorWithNext)
newControlPoints[index + 1] = newControlPoints[index];
if (isSegmentSeparatorWithPrevious)
newControlPoints[index - 1] = newControlPoints[index];
slider.ControlPoints = newControlPoints;
slider.Path.Calculate(true);
return true;
}
protected override bool OnDragEnd(DragEndEvent e) => true;
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
private bool isSegmentSeparatorWithNext => index < slider.ControlPoints.Length - 1 && slider.ControlPoints[index + 1] == slider.ControlPoints[index];
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.ControlPoints[index - 1] == slider.ControlPoints[index];
}
}
@@ -0,0 +1,34 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components
{
public class PathControlPointVisualiser : CompositeDrawable
{
private readonly Slider slider;
private readonly Container<PathControlPointPiece> pieces;
public PathControlPointVisualiser(Slider slider)
{
this.slider = slider;
InternalChild = pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
slider.ControlPointsChanged += _ => updatePathControlPoints();
updatePathControlPoints();
}
private void updatePathControlPoints()
{
while (slider.ControlPoints.Length > pieces.Count)
pieces.Add(new PathControlPointPiece(slider, pieces.Count));
while (slider.ControlPoints.Length < pieces.Count)
pieces.Remove(pieces[pieces.Count - 1]);
}
}
}
@@ -0,0 +1,59 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components
{
public class SliderBodyPiece : CompositeDrawable
{
private readonly Slider slider;
private readonly ManualSliderBody body;
public SliderBodyPiece(Slider slider)
{
this.slider = slider;
InternalChild = body = new ManualSliderBody
{
AccentColour = Color4.Transparent,
PathWidth = slider.Scale * 64
};
slider.PositionChanged += _ => updatePosition();
slider.ScaleChanged += _ => body.PathWidth = slider.Scale * 64;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
body.BorderColour = colours.Yellow;
updatePosition();
}
private void updatePosition() => Position = slider.StackedPosition;
protected override void Update()
{
base.Update();
slider.Path.Calculate();
var vertices = new List<Vector2>();
slider.Path.GetPathToProgress(vertices, 0, 1);
body.SetVertices(vertices);
Size = body.Size;
OriginPosition = body.PathOffset;
}
}
}

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