1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 12:17:26 +08:00

Merge branch 'master' into catch-hyperdash-catcher-colouring

This commit is contained in:
Bartłomiej Dach 2020-05-10 16:49:44 +02:00
commit 77e5e131c9
221 changed files with 3118 additions and 1194 deletions

View File

@ -4,3 +4,5 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.

View File

@ -16,9 +16,9 @@
<EmbeddedResource Include="Resources\**\*.*" /> <EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Code Analysis"> <ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.0.0" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Code Analysis"> <PropertyGroup Label="Code Analysis">
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet>

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.412.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.427.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.421.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.508.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -18,7 +18,8 @@ namespace osu.Android
try try
{ {
string versionName = packageInfo.VersionCode.ToString(); // todo: needs checking before play store redeploy.
string versionName = packageInfo.VersionName;
// undo play store version garbling // undo play store version garbling
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1))); return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
} }

View File

@ -6,15 +6,14 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Win32;
using osu.Desktop.Overlays; using osu.Desktop.Overlays;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osuTK.Input; using osuTK.Input;
using Microsoft.Win32;
using osu.Desktop.Updater; using osu.Desktop.Updater;
using osu.Framework; using osu.Framework;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform.Windows;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Updater; using osu.Game.Updater;
@ -37,7 +36,11 @@ namespace osu.Desktop
try try
{ {
if (Host is DesktopGameHost desktopHost) if (Host is DesktopGameHost desktopHost)
return new StableStorage(desktopHost); {
string stablePath = getStableInstallPath();
if (!string.IsNullOrEmpty(stablePath))
return new DesktopStorage(stablePath, desktopHost);
}
} }
catch (Exception) catch (Exception)
{ {
@ -47,6 +50,35 @@ namespace osu.Desktop
return null; return null;
} }
private string getStableInstallPath()
{
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
string stableInstallPath;
try
{
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
if (checkExists(stableInstallPath))
return stableInstallPath;
}
catch
{
}
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
if (checkExists(stableInstallPath))
return stableInstallPath;
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
if (checkExists(stableInstallPath))
return stableInstallPath;
return null;
}
protected override UpdateManager CreateUpdateManager() protected override UpdateManager CreateUpdateManager()
{ {
switch (RuntimeInfo.OS) switch (RuntimeInfo.OS)
@ -111,45 +143,5 @@ namespace osu.Desktop
Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
} }
/// <summary>
/// A method of accessing an osu-stable install in a controlled fashion.
/// </summary>
private class StableStorage : WindowsStorage
{
protected override string LocateBasePath()
{
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs"));
string stableInstallPath;
try
{
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
if (checkExists(stableInstallPath))
return stableInstallPath;
}
catch
{
}
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
if (checkExists(stableInstallPath))
return stableInstallPath;
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu");
if (checkExists(stableInstallPath))
return stableInstallPath;
return null;
}
public StableStorage(DesktopGameHost host)
: base(string.Empty, host)
{
}
}
} }
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.2058561036909863d, "diffcalc-test")] [TestCase(4.050601681491468d, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public class CatchDifficultyCalculator : DifficultyCalculator public class CatchDifficultyCalculator : DifficultyCalculator
{ {
private const double star_scaling_factor = 0.145; private const double star_scaling_factor = 0.153;
protected override int SectionLength => 750; protected override int SectionLength => 750;
@ -73,6 +73,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
return new Skill[] return new Skill[]
{ {
new Movement(halfCatcherWidth), new Movement(halfCatcherWidth),

View File

@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Longer maps are worth more // Longer maps are worth more
double lengthBonus = double lengthBonus =
0.95 + 0.4 * Math.Min(1.0, numTotalHits / 3000.0) + 0.95f + 0.3f * Math.Min(1.0f, numTotalHits / 2500.0f) +
(numTotalHits > 3000 ? Math.Log10(numTotalHits / 3000.0) * 0.5 : 0.0); (numTotalHits > 2500 ? (float)Math.Log10(numTotalHits / 2500.0f) * 0.475f : 0.0f);
// Longer maps are worth more // Longer maps are worth more
value *= lengthBonus; value *= lengthBonus;
@ -63,19 +63,28 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Combo scaling // Combo scaling
if (Attributes.MaxCombo > 0) if (Attributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0; float approachRate = (float)Attributes.ApproachRate;
if (Attributes.ApproachRate > 9.0) float approachRateFactor = 1.0f;
approachRateFactor += 0.1 * (Attributes.ApproachRate - 9.0); // 10% for each AR above 9 if (approachRate > 9.0f)
else if (Attributes.ApproachRate < 8.0) approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9
approachRateFactor += 0.025 * (8.0 - Attributes.ApproachRate); // 2.5% for each AR below 8 if (approachRate > 10.0f)
approachRateFactor += 0.1f * (approachRate - 10.0f); // Additional 10% at AR 11, 30% total
else if (approachRate < 8.0f)
approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8
value *= approachRateFactor; value *= approachRateFactor;
if (mods.Any(m => m is ModHidden)) if (mods.Any(m => m is ModHidden))
// Hiddens gives nothing on max approach rate, and more the lower it is {
value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10 value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
// Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0f)
value *= 1.05f + 0.075f * (10.0f - approachRate); // 7.5% for each AR below 10
else if (approachRate > 10.0f)
value *= 1.01f + 0.04f * (11.0f - Math.Min(11.0f, approachRate)); // 5% at AR 10, 1% at AR 11
}
if (mods.Any(m => m is ModFlashlight)) if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.

View File

@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
public readonly float LastNormalizedPosition; public readonly float LastNormalizedPosition;
/// <summary> /// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 25ms. /// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
/// </summary> /// </summary>
public readonly double StrainTime; public readonly double StrainTime;
public readonly double ClockRate;
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate) : base(hitObject, lastObject, clockRate)
{ {
@ -34,8 +36,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
// Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(25, DeltaTime); StrainTime = Math.Max(40, DeltaTime);
ClockRate = clockRate;
} }
} }
} }

View File

@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{ {
private const float absolute_player_positioning_error = 16f; private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f; private const float normalized_hitobject_radius = 41.0f;
private const double direction_change_bonus = 12.5; private const double direction_change_bonus = 21.0;
protected override double SkillMultiplier => 850; protected override double SkillMultiplier => 900;
protected override double StrainDecayBase => 0.2; protected override double StrainDecayBase => 0.2;
protected override double DecayWeight => 0.94; protected override double DecayWeight => 0.94;
@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float? lastPlayerPosition; private float? lastPlayerPosition;
private float lastDistanceMoved; private float lastDistanceMoved;
private double lastStrainTime;
public Movement(float halfCatcherWidth) public Movement(float halfCatcherWidth)
{ {
@ -45,47 +46,47 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value; float distanceMoved = playerPosition - lastPlayerPosition.Value;
double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 500; double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate);
double sqrtStrain = Math.Sqrt(catchCurrent.StrainTime);
double bonus = 0; double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
double sqrtStrain = Math.Sqrt(weightedStrainTime);
// Direction changes give an extra point! double edgeDashBonus = 0;
// Direction change bonus.
if (Math.Abs(distanceMoved) > 0.1) if (Math.Abs(distanceMoved) > 0.1)
{ {
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved)) if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
{ {
double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(distanceMoved)) / absolute_player_positioning_error; double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor; distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
// Bonus for tougher direction switches and "almost" hyperdashes at this point
if (catchCurrent.LastObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH)
bonus = 0.3 * bonusFactor;
} }
// Base bonus for every movement, giving some weight to streams. // Base bonus for every movement, giving some weight to streams.
distanceAddition += 7.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
} }
// Bonus for "almost" hyperdashes at corner points // Bonus for edge dashes.
if (catchCurrent.LastObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH) if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH)
{ {
if (!catchCurrent.LastObject.HyperDash) if (!catchCurrent.LastObject.HyperDash)
bonus += 1.0; edgeDashBonus += 5.7;
else else
{ {
// After a hyperdash we ARE in the correct position. Always! // After a hyperdash we ARE in the correct position. Always!
playerPosition = catchCurrent.NormalizedPosition; playerPosition = catchCurrent.NormalizedPosition;
} }
distanceAddition *= 1.0 + bonus * ((10 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10); distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
} }
lastPlayerPosition = playerPosition; lastPlayerPosition = playerPosition;
lastDistanceMoved = distanceMoved; lastDistanceMoved = distanceMoved;
lastStrainTime = catchCurrent.StrainTime;
return distanceAddition / catchCurrent.StrainTime; return distanceAddition / weightedStrainTime;
} }
} }
} }

View File

@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Mania.Tests
AccentColour = Color4.OrangeRed, AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll Clock = new FramedClock(new StopwatchClock()), // No scroll
}); });
AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
} }
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };

View File

@ -0,0 +1,221 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneManiaHitObjectComposer : EditorClockTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ManiaBlueprintContainer)
};
private TestComposer composer;
[SetUp]
public void Setup() => Schedule(() =>
{
BeatDivisor.Value = 8;
Clock.Seek(0);
Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both };
});
[Test]
public void TestDragOffscreenSelectionVerticallyUpScroll()
{
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Up);
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
AddStep("click last object", () =>
{
originalPosition = lastObject.DrawPosition;
InputManager.MoveMouseTo(lastObject);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move mouse downwards", () =>
{
InputManager.MoveMouseTo(lastObject, new Vector2(0, 20));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0);
AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50);
}
[Test]
public void TestDragOffscreenSelectionVerticallyDownScroll()
{
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Down);
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
AddStep("click last object", () =>
{
originalPosition = lastObject.DrawPosition;
InputManager.MoveMouseTo(lastObject);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move mouse upwards", () =>
{
InputManager.MoveMouseTo(lastObject, new Vector2(0, -20));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0);
AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50);
}
[Test]
public void TestDragOffscreenSelectionHorizontally()
{
DrawableHitObject lastObject = null;
Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Down);
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType<DrawableHitObject>().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects));
AddStep("click last object", () =>
{
originalPosition = lastObject.DrawPosition;
InputManager.MoveMouseTo(lastObject);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move mouse right", () =>
{
var firstColumn = composer.Composer.Playfield.GetColumn(0);
var secondColumn = composer.Composer.Playfield.GetColumn(1);
InputManager.MoveMouseTo(lastObject, new Vector2(secondColumn.ScreenSpaceDrawQuad.Centre.X - firstColumn.ScreenSpaceDrawQuad.Centre.X + 1, 0));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 1));
// Todo: They'll move vertically by the height of a note since there's no snapping and the selection point is the middle of the note.
AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y <= DefaultNotePiece.NOTE_HEIGHT);
}
[Test]
public void TestDragHoldNoteSelectionVertically()
{
setScrollStep(ScrollingDirection.Down);
AddStep("setup beatmap", () =>
{
composer.EditorBeatmap.Clear();
composer.EditorBeatmap.Add(new HoldNote
{
Column = 1,
EndTime = 200
});
});
DrawableHoldNote holdNote = null;
AddStep("grab hold note", () =>
{
holdNote = this.ChildrenOfType<DrawableHoldNote>().Single();
InputManager.MoveMouseTo(holdNote);
InputManager.PressButton(MouseButton.Left);
});
AddStep("move drag upwards", () =>
{
InputManager.MoveMouseTo(holdNote, new Vector2(0, -100));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteSelectionBlueprint>().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition);
AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType<HoldNoteNoteSelectionBlueprint>().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition);
}
private void setScrollStep(ScrollingDirection direction)
=> AddStep($"set scroll direction = {direction}", () => ((Bindable<ScrollingDirection>)composer.Composer.ScrollingInfo.Direction).Value = direction);
private class TestComposer : CompositeDrawable
{
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
public readonly EditorBeatmap EditorBeatmap;
public readonly ManiaHitObjectComposer Composer;
public TestComposer()
{
InternalChildren = new Drawable[]
{
EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
},
Composer = new ManiaHitObjectComposer(new ManiaRuleset())
};
for (int i = 0; i < 10; i++)
EditorBeatmap.Add(new Note { StartTime = 100 * i });
}
}
}
}

View File

@ -1,17 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
{ {
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{ {
[SetUp]
public void Setup() => Schedule(() =>
{
this.ChildrenOfType<HitObjectContainer>().ForEach(c => c.Clear());
ResetPlacement();
((ScrollingTestContainer)HitObjectContainer).Direction = ScrollingDirection.Down;
});
[Test]
public void TestPlaceBeforeCurrentTimeDownwards()
{
AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time < 0", () => getNote().StartTime < 0);
}
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time > 0", () => getNote().StartTime > 0);
}
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint(); protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osuTK; using osuTK;
using osu.Game.Audio;
namespace osu.Game.Rulesets.Mania.Beatmaps namespace osu.Game.Rulesets.Mania.Beatmaps
{ {
@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
} }
} }
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject); public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original) protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
{ {
@ -239,8 +238,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
StartTime = HitObject.StartTime, StartTime = HitObject.StartTime,
Duration = endTimeData.Duration, Duration = endTimeData.Duration,
Column = column, Column = column,
Head = { Samples = sampleInfoListAt(HitObject.StartTime) }, Samples = HitObject.Samples,
Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) }, NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
}); });
} }
else if (HitObject is IHasXPosition) else if (HitObject is IHasXPosition)
@ -255,22 +254,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern; return pattern;
} }
/// <summary>
/// Retrieves the sample info list at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private IList<HitSampleInfo> sampleInfoListAt(double time)
{
if (!(HitObject is IHasCurve curveData))
return HitObject.Samples;
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
return curveData.NodeSamples[index];
}
} }
} }
} }

View File

@ -505,16 +505,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
} }
else else
{ {
var holdNote = new HoldNote newObject = new HoldNote
{ {
StartTime = startTime, StartTime = startTime,
Column = column,
Duration = endTime - startTime, Duration = endTime - startTime,
Head = { Samples = sampleInfoListAt(startTime) }, Column = column,
Tail = { Samples = sampleInfoListAt(endTime) } Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
}; };
newObject = holdNote;
} }
pattern.Add(newObject); pattern.Add(newObject);

View File

@ -64,21 +64,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (holdNote) if (holdNote)
{ {
var hold = new HoldNote newObject = new HoldNote
{ {
StartTime = HitObject.StartTime, StartTime = HitObject.StartTime,
Duration = endTime - HitObject.StartTime,
Column = column, Column = column,
Duration = endTime - HitObject.StartTime Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
}; };
if (hold.Head.Samples == null)
hold.Head.Samples = new List<HitSampleInfo>();
hold.Head.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL });
hold.Tail.Samples = HitObject.Samples;
newObject = hold;
} }
else else
{ {

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osuTK; using osuTK;
@ -46,6 +47,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
bodyPiece.Height = (bottomPosition - topPosition).Y; bodyPiece.Height = (bottomPosition - topPosition).Y;
} }
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
EndPlacement(true);
}
private double originalStartTime; private double originalStartTime;
public override void UpdatePosition(Vector2 screenSpacePosition) public override void UpdatePosition(Vector2 screenSpacePosition)

View File

@ -76,5 +76,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
} }
public override Quad SelectionQuad => ScreenSpaceDrawQuad; public override Quad SelectionQuad => ScreenSpaceDrawQuad;
public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
} }
} }

View File

@ -50,16 +50,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return base.OnMouseDown(e); return base.OnMouseDown(e);
HitObject.Column = Column.Index; HitObject.Column = Column.Index;
BeginPlacement(TimeAt(e.ScreenSpaceMousePosition)); BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
return true; return true;
} }
protected override void OnMouseUp(MouseUpEvent e)
{
EndPlacement(true);
base.OnMouseUp(e);
}
public override void UpdatePosition(Vector2 screenSpacePosition) public override void UpdatePosition(Vector2 screenSpacePosition)
{ {
if (!PlacementActive) if (!PlacementActive)

View File

@ -3,8 +3,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -15,13 +13,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public class ManiaSelectionBlueprint : OverlaySelectionBlueprint public class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{ {
public Vector2 ScreenSpaceDragPosition { get; private set; }
public Vector2 DragPosition { get; private set; }
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject; public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
protected IClock EditorClock { get; private set; }
[Resolved] [Resolved]
private IScrollingInfo scrollingInfo { get; set; } private IScrollingInfo scrollingInfo { get; set; }
@ -34,12 +27,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
RelativeSizeAxes = Axes.None; RelativeSizeAxes = Axes.None;
} }
[BackgroundDependencyLoader]
private void load(IAdjustableClock clock)
{
EditorClock = clock;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -47,22 +34,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero)); Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero));
} }
protected override bool OnMouseDown(MouseDownEvent e)
{
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
return base.OnMouseDown(e);
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
ScreenSpaceDragPosition = e.ScreenSpaceMousePosition;
DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition);
}
public override void Show() public override void Show()
{ {
DrawableObject.AlwaysAlive = true; DrawableObject.AlwaysAlive = true;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
@ -26,5 +27,15 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
Width = SnappedWidth; Width = SnappedWidth;
Position = SnappedMousePosition; Position = SnappedMousePosition;
} }
protected override bool OnMouseDown(MouseDownEvent e)
{
base.OnMouseDown(e);
// Place the note immediately.
EndPlacement(true);
return true;
}
} }
} }

View File

@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Edit
return base.CreateBlueprintFor(hitObject); return base.CreateBlueprintFor(hitObject);
} }
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -37,7 +38,33 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
public int TotalColumns => ((ManiaPlayfield)drawableRuleset.Playfield).TotalColumns; public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
public int TotalColumns => Playfield.TotalColumns;
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time)
{
var hoc = Playfield.GetColumn(0).HitObjectContainer;
float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y;
if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down)
{
// We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
// The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
// so when scrolling downwards the coordinates need to be flipped.
targetPosition = hoc.DrawHeight - targetPosition;
}
double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition,
EditorClock.CurrentTime,
drawableRuleset.ScrollingInfo.TimeRange.Value,
hoc.DrawHeight);
return base.GetSnappedPosition(position, targetTime);
}
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{ {

View File

@ -4,11 +4,8 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
@ -22,85 +19,16 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved] [Resolved]
private IManiaHitObjectComposer composer { get; set; } private IManiaHitObjectComposer composer { get; set; }
private IClock editorClock;
[BackgroundDependencyLoader]
private void load(IAdjustableClock clock)
{
editorClock = clock;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent) public override bool HandleMovement(MoveSelectionEvent moveEvent)
{ {
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint; var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column; int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
adjustOrigins(maniaBlueprint);
performDragMovement(moveEvent);
performColumnMovement(lastColumn, moveEvent); performColumnMovement(lastColumn, moveEvent);
return true; return true;
} }
/// <summary>
/// Ensures that the position of hitobjects remains centred to the mouse position.
/// E.g. The hitobject position will change if the editor scrolls while a hitobject is dragged.
/// </summary>
/// <param name="reference">The <see cref="ManiaSelectionBlueprint"/> that received the drag event.</param>
private void adjustOrigins(ManiaSelectionBlueprint reference)
{
var referenceParent = (HitObjectContainer)reference.DrawableObject.Parent;
float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.DrawableObject.OriginPosition.Y;
float targetPosition = referenceParent.ToLocalSpace(reference.ScreenSpaceDragPosition).Y - offsetFromReferenceOrigin;
// Flip the vertical coordinate space when scrolling downwards
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
targetPosition -= referenceParent.DrawHeight;
float movementDelta = targetPosition - reference.DrawableObject.Position.Y;
foreach (var b in SelectedBlueprints.OfType<ManiaSelectionBlueprint>())
b.DrawableObject.Y += movementDelta;
}
private void performDragMovement(MoveSelectionEvent moveEvent)
{
float delta = moveEvent.InstantDelta.Y;
// When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen.
// This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height.
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: probably wrong
foreach (var selectionBlueprint in SelectedBlueprints)
{
var b = (OverlaySelectionBlueprint)selectionBlueprint;
var hitObject = b.DrawableObject;
var objectParent = (HitObjectContainer)hitObject.Parent;
// StartTime could be used to adjust the position if only one movement event was received per frame.
// However this is not the case and ScrollingHitObjectContainer performs movement in UpdateAfterChildren() so the position must also be updated to be valid for further movement events
hitObject.Y += delta;
float targetPosition = hitObject.Position.Y;
// The scrolling algorithm always assumes an anchor at the top of the screen, so the position must be flipped when scrolling downwards to reflect a top anchor
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
targetPosition = -targetPosition;
objectParent.Remove(hitObject);
hitObject.HitObject.StartTime = scrollingInfo.Algorithm.TimeAt(targetPosition,
editorClock.CurrentTime,
scrollingInfo.TimeRange.Value,
objectParent.DrawHeight);
objectParent.Add(hitObject);
}
}
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent) private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{ {
var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition); var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition);

View File

@ -1,18 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Edit.Masks
{
public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{
protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject)
{
RelativeSizeAxes = Axes.None;
}
}
}

View File

@ -51,7 +51,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece()) bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
{
RelativeSizeAxes = Axes.Both
})
{ {
RelativeSizeAxes = Axes.X RelativeSizeAxes = Axes.X
}, },
@ -127,6 +130,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
} }
public override void PlaySamples()
{
// Samples are played by the head/tail notes.
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();

View File

@ -13,11 +13,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
public abstract class DrawableManiaHitObject : DrawableHitObject<ManiaHitObject> public abstract class DrawableManiaHitObject : DrawableHitObject<ManiaHitObject>
{ {
/// <summary>
/// Whether this <see cref="DrawableManiaHitObject"/> should always remain alive.
/// </summary>
internal bool AlwaysAlive;
/// <summary> /// <summary>
/// The <see cref="ManiaAction"/> which causes this <see cref="DrawableManiaHitObject{TObject}"/> to be hit. /// The <see cref="ManiaAction"/> which causes this <see cref="DrawableManiaHitObject{TObject}"/> to be hit.
/// </summary> /// </summary>
@ -54,7 +49,62 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Direction.BindValueChanged(OnDirectionChanged, true); Direction.BindValueChanged(OnDirectionChanged, true);
} }
protected override bool ShouldBeAlive => AlwaysAlive || base.ShouldBeAlive; private double computedLifetimeStart;
public override double LifetimeStart
{
get => base.LifetimeStart;
set
{
computedLifetimeStart = value;
if (!AlwaysAlive)
base.LifetimeStart = value;
}
}
private double computedLifetimeEnd;
public override double LifetimeEnd
{
get => base.LifetimeEnd;
set
{
computedLifetimeEnd = value;
if (!AlwaysAlive)
base.LifetimeEnd = value;
}
}
private bool alwaysAlive;
/// <summary>
/// Whether this <see cref="DrawableManiaHitObject"/> should always remain alive.
/// </summary>
internal bool AlwaysAlive
{
get => alwaysAlive;
set
{
if (alwaysAlive == value)
return;
alwaysAlive = value;
if (value)
{
// Set the base lifetimes directly, to avoid mangling the computed lifetimes
base.LifetimeStart = double.MinValue;
base.LifetimeEnd = double.MaxValue;
}
else
{
LifetimeStart = computedLifetimeStart;
LifetimeEnd = computedLifetimeEnd;
}
}
}
protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e) protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{ {

View File

@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
public DefaultBodyPiece() public DefaultBodyPiece()
{ {
RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive; Blending = BlendingParameters.Additive;
AddLayout(subtractionCache); AddLayout(subtractionCache);

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -28,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.Objects
set set
{ {
duration = value; duration = value;
if (Tail != null)
Tail.StartTime = EndTime; Tail.StartTime = EndTime;
} }
} }
@ -38,7 +42,11 @@ namespace osu.Game.Rulesets.Mania.Objects
set set
{ {
base.StartTime = value; base.StartTime = value;
if (Head != null)
Head.StartTime = value; Head.StartTime = value;
if (Tail != null)
Tail.StartTime = EndTime; Tail.StartTime = EndTime;
} }
} }
@ -49,20 +57,26 @@ namespace osu.Game.Rulesets.Mania.Objects
set set
{ {
base.Column = value; base.Column = value;
if (Head != null)
Head.Column = value; Head.Column = value;
if (Tail != null)
Tail.Column = value; Tail.Column = value;
} }
} }
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
/// <summary> /// <summary>
/// The head note of the hold. /// The head note of the hold.
/// </summary> /// </summary>
public readonly Note Head = new Note(); public Note Head { get; private set; }
/// <summary> /// <summary>
/// The tail note of the hold. /// The tail note of the hold.
/// </summary> /// </summary>
public readonly TailNote Tail = new TailNote(); public TailNote Tail { get; private set; }
/// <summary> /// <summary>
/// The time between ticks of this hold. /// The time between ticks of this hold.
@ -83,8 +97,19 @@ namespace osu.Game.Rulesets.Mania.Objects
createTicks(); createTicks();
AddNested(Head); AddNested(Head = new Note
AddNested(Tail); {
StartTime = StartTime,
Column = Column,
Samples = getNodeSamples(0),
});
AddNested(Tail = new TailNote
{
StartTime = EndTime,
Column = Column,
Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
} }
private void createTicks() private void createTicks()
@ -105,5 +130,8 @@ namespace osu.Game.Rulesets.Mania.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
} }
} }

View File

@ -5,11 +5,12 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.Mania.Objects.Types; using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects namespace osu.Game.Rulesets.Mania.Objects
{ {
public abstract class ManiaHitObject : HitObject, IHasColumn public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{ {
public readonly Bindable<int> ColumnBindable = new Bindable<int>(); public readonly Bindable<int> ColumnBindable = new Bindable<int>();
@ -20,5 +21,11 @@ namespace osu.Game.Rulesets.Mania.Objects
} }
protected override HitWindows CreateHitWindows() => new ManiaHitWindows(); protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
#region LegacyBeatmapEncoder
float IHasXPosition.X => Column;
#endregion
} }
} }

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -48,6 +50,10 @@ namespace osu.Game.Rulesets.Mania.UI
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly Bindable<double> configTimeRange = new BindableDouble();
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
@ -58,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
foreach (var mod in Mods.OfType<IApplicableToTrack>())
mod.ApplyToTrack(speedAdjustmentTrack);
bool isForCurrentRuleset = Beatmap.BeatmapInfo.Ruleset.Equals(Ruleset.RulesetInfo); bool isForCurrentRuleset = Beatmap.BeatmapInfo.Ruleset.Equals(Ruleset.RulesetInfo);
foreach (var p in ControlPoints) foreach (var p in ControlPoints)
@ -76,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection); Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange); Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
} }
protected override void AdjustScrollSpeed(int amount) protected override void AdjustScrollSpeed(int amount)
@ -86,10 +95,19 @@ namespace osu.Game.Rulesets.Mania.UI
private double relativeTimeRange private double relativeTimeRange
{ {
get => MAX_TIME_RANGE / TimeRange.Value; get => MAX_TIME_RANGE / configTimeRange.Value;
set => TimeRange.Value = MAX_TIME_RANGE / value; set => configTimeRange.Value = MAX_TIME_RANGE / value;
} }
protected override void Update()
{
base.Update();
updateTimeRange();
}
private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
/// <summary> /// <summary>
/// Retrieves the column that intersects a screen-space position. /// Retrieves the column that intersects a screen-space position.
/// </summary> /// </summary>

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
foreach (var column in stage.Columns) foreach (var column in stage.Columns)
{ {
if (column.ReceivePositionalInputAt(screenSpacePosition)) if (column.ReceivePositionalInputAt(new Vector2(screenSpacePosition.X, column.ScreenSpaceDrawQuad.Centre.Y)))
{ {
found = column; found = column;
break; break;
@ -87,6 +87,31 @@ namespace osu.Game.Rulesets.Mania.UI
return found; return found;
} }
/// <summary>
/// Retrieves a <see cref="Column"/> by index.
/// </summary>
/// <param name="index">The index of the column.</param>
/// <returns>The <see cref="Column"/> corresponding to the given index.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="index"/> is less than 0 or greater than <see cref="TotalColumns"/>.</exception>
public Column GetColumn(int index)
{
if (index < 0 || index > TotalColumns - 1)
throw new ArgumentOutOfRangeException(nameof(index));
foreach (var stage in stages)
{
if (index >= stage.Columns.Count)
{
index -= stage.Columns.Count;
continue;
}
return stage.Columns[index];
}
throw new ArgumentOutOfRangeException(nameof(index));
}
/// <summary> /// <summary>
/// Retrieves the total amount of columns across all stages in this playfield. /// Retrieves the total amount of columns across all stages in this playfield.
/// </summary> /// </summary>

View File

@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public TestSlider() public TestSlider()
{ {
DefaultsApplied += () => DefaultsApplied += _ =>
{ {
HeadCircle.HitWindows = new TestHitWindows(); HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows(); TailCircle.HitWindows = new TestHitWindows();

View File

@ -259,6 +259,23 @@ namespace osu.Game.Rulesets.Osu.Tests
assertControlPointType(2, PathType.PerfectCurve); assertControlPointType(2, PathType.PerfectCurve);
} }
[Test]
public void TestBeginPlacementWithoutReleasingMouse()
{
addMovementStep(new Vector2(200));
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(400, 200));
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
}
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
private void addClickStep(MouseButton button) private void addClickStep(MouseButton button)

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -6,6 +6,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
{ {
@ -28,16 +29,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
circlePiece.UpdateFrom(HitObject); circlePiece.UpdateFrom(HitObject);
} }
protected override bool OnClick(ClickEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
{ {
EndPlacement(true); EndPlacement(true);
return true; return true;
} }
public override void UpdatePosition(Vector2 screenSpacePosition) return base.OnMouseDown(e);
{ }
BeginPlacement();
HitObject.Position = ToLocalSpace(screenSpacePosition); public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition);
}
} }
} }

View File

@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection; public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
public readonly BindableBool IsSelected = new BindableBool(); public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
private readonly Slider slider; private readonly Slider slider;
@ -146,6 +145,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
{ {
if (RequestSelection == null)
return false;
if (e.Button == MouseButton.Left) if (e.Button == MouseButton.Left)
{ {
changeHandler?.BeginChange(); changeHandler?.BeginChange();

View File

@ -82,8 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
} }
protected override bool OnClick(ClickEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (e.Button != MouseButton.Left)
return base.OnMouseDown(e);
switch (state) switch (state)
{ {
case PlacementState.Initial: case PlacementState.Initial:
@ -91,9 +94,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
break; break;
case PlacementState.Body: case PlacementState.Body:
if (e.Button != MouseButton.Left)
break;
if (canPlaceNewControlPoint(out var lastPoint)) if (canPlaceNewControlPoint(out var lastPoint))
{ {
// Place a new point by detatching the current cursor. // Place a new point by detatching the current cursor.
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
currentSegmentLength = 1; currentSegmentLength = 1;
} }
return true; break;
} }
return true; return true;

View File

@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void bindEvents(DrawableOsuHitObject drawableObject) private void bindEvents(DrawableOsuHitObject drawableObject)
{ {
drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh()); drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
drawableObject.HitObject.DefaultsApplied += scheduleRefresh; drawableObject.HitObject.DefaultsApplied += _ => scheduleRefresh();
} }
private void scheduleRefresh() private void scheduleRefresh()

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
internal class DrawableTestHit : DrawableTaikoHitObject
{
private readonly HitResult type;
public DrawableTestHit(Hit hit, HitResult type = HitResult.Great)
: base(hit)
{
this.type = type;
}
[BackgroundDependencyLoader]
private void load()
{
Result.Type = type;
}
public override bool OnPressed(TaikoAction action) => false;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,111 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableBarLine : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableBarLine),
typeof(LegacyBarLine),
typeof(BarLine),
}).ToList();
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 5000 },
};
[BackgroundDependencyLoader]
private void load()
{
AddStep("Bar line", () => SetContents(() =>
{
ScrollingHitObjectContainer hoc;
var cont = new Container
{
RelativeSizeAxes = Axes.Both,
Height = 0.8f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new TaikoPlayfield(new ControlPointInfo()),
hoc = new ScrollingHitObjectContainer()
}
};
hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
return cont;
}));
AddStep("Bar line (major)", () => SetContents(() =>
{
ScrollingHitObjectContainer hoc;
var cont = new Container
{
RelativeSizeAxes = Axes.Both,
Height = 0.8f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new TaikoPlayfield(new ControlPointInfo()),
hoc = new ScrollingHitObjectContainer()
}
};
hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
return cont;
}));
}
private BarLine createBarLineAtCurrentTime(bool major = false)
{
var barline = new BarLine
{
Major = major,
StartTime = Time.Current + 2000,
};
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
barline.ApplyDefaults(cpi, new BeatmapDifficulty());
return barline;
}
}
}

View File

@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{ {
typeof(DrawableHit), typeof(DrawableHit),
typeof(DrawableCentreHit),
typeof(DrawableRimHit),
typeof(LegacyHit), typeof(LegacyHit),
typeof(LegacyCirclePiece), typeof(LegacyCirclePiece),
}).ToList(); }).ToList();
@ -30,25 +28,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
AddStep("Centre hit", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime()) AddStep("Centre hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
})); }));
AddStep("Centre hit (strong)", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime(true)) AddStep("Centre hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
})); }));
AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime()) AddStep("Rim hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
})); }));
AddStep("Rim hit (strong)", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime(true)) AddStep("Rim hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -0,0 +1,58 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(HitExplosion),
typeof(LegacyHitExplosion),
typeof(DefaultHitExplosion),
}).ToList();
[BackgroundDependencyLoader]
private void load()
{
AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
}
private Drawable getContentFor(HitResult type)
{
DrawableTaikoHitObject hit;
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hit = createHit(type),
new HitExplosion(hit)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
}
private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
}
}

View File

@ -5,8 +5,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -18,8 +21,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{ {
typeof(HitTarget), typeof(TaikoHitTarget),
typeof(LegacyHitTarget), typeof(TaikoLegacyHitTarget),
typeof(PlayfieldBackgroundRight),
}).ToList(); }).ToList();
[Cached(typeof(IScrollingInfo))] [Cached(typeof(IScrollingInfo))]
@ -31,12 +35,30 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
public TestSceneTaikoPlayfield() public TestSceneTaikoPlayfield()
{ {
TaikoBeatmap beatmap;
bool kiai = false;
AddStep("set beatmap", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap = new TaikoBeatmap());
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
Beatmap.Value.Track.Start();
});
AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo()) AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo())
{ {
Height = 0.4f,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
})); }));
AddRepeatStep("change height", () => this.ChildrenOfType<TaikoPlayfield>().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50);
AddStep("Toggle kiai", () =>
{
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { KiaiMode = (kiai = !kiai) });
});
} }
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestFixture] [TestFixture]
public class TestSceneHits : OsuTestScene public class TestSceneHits : OsuTestScene
{ {
private const double default_duration = 1000; private const double default_duration = 3000;
private const float scroll_time = 1000; private const float scroll_time = 1000;
protected override double TimePerAction => default_duration * 2; protected override double TimePerAction => default_duration * 2;
@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Miss :(", addMissJudgement); AddStep("Miss :(", addMissJudgement);
AddStep("DrumRoll", () => addDrumRoll(false)); AddStep("DrumRoll", () => addDrumRoll(false));
AddStep("Strong DrumRoll", () => addDrumRoll(true)); AddStep("Strong DrumRoll", () => addDrumRoll(true));
AddStep("Kiai DrumRoll", () => addDrumRoll(true, kiai: true));
AddStep("Swell", () => addSwell()); AddStep("Swell", () => addSwell());
AddStep("Centre", () => addCentreHit(false)); AddStep("Centre", () => addCentreHit(false));
AddStep("Strong Centre", () => addCentreHit(true)); AddStep("Strong Centre", () => addCentreHit(true));
@ -148,6 +149,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
Add(h);
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
} }
@ -163,6 +166,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
Add(h);
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
} }
@ -192,7 +197,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
drawableRuleset.Playfield.Add(new DrawableSwell(swell)); drawableRuleset.Playfield.Add(new DrawableSwell(swell));
} }
private void addDrumRoll(bool strong, double duration = default_duration) private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false)
{ {
addBarLine(true); addBarLine(true);
addBarLine(true, scroll_time + duration); addBarLine(true, scroll_time + duration);
@ -202,9 +207,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong, IsStrong = strong,
Duration = duration, Duration = duration,
TickRate = 8,
}; };
d.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); var cpi = new ControlPointInfo();
cpi.Add(-10000, new EffectControlPoint { KiaiMode = kiai });
d.ApplyDefaults(cpi, new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); drawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
} }
@ -219,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableCentreHit(h)); drawableRuleset.Playfield.Add(new DrawableHit(h));
} }
private void addRimHit(bool strong) private void addRimHit(bool strong)
@ -232,7 +241,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableRuleset.Playfield.Add(new DrawableRimHit(h)); drawableRuleset.Playfield.Add(new DrawableHit(h));
} }
private class TestStrongNestedHit : DrawableStrongNestedHit private class TestStrongNestedHit : DrawableStrongNestedHit
@ -244,13 +253,5 @@ namespace osu.Game.Rulesets.Taiko.Tests
public override bool OnPressed(TaikoAction action) => false; public override bool OnPressed(TaikoAction action) => false;
} }
private class DrawableTestHit : DrawableHitObject<TaikoHitObject>
{
public DrawableTestHit(TaikoHitObject hitObject)
: base(hitObject)
{
}
}
} }
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
/// osu! is generally slower than taiko, so a factor is added to increase /// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used. /// speed. This must be used everywhere slider length or beat length is used.
/// </summary> /// </summary>
private const float legacy_velocity_multiplier = 1.4f; public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
/// <summary> /// <summary>
/// Because swells are easier in taiko than spinners are in osu!, /// Because swells are easier in taiko than spinners are in osu!,
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// Rewrite the beatmap info to add the slider velocity multiplier // Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone(); original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone(); original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier; original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original); Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original);
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment; double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later // The true distance, accounting for any repeats. This ends up being the drum roll distance later
double distance = distanceData.Distance * spans * legacy_velocity_multiplier; double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll // The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength; double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// <summary> /// <summary>
/// The visual line tracker. /// The visual line tracker.
/// </summary> /// </summary>
protected Box Tracker; protected SkinnableDrawable Line;
/// <summary> /// <summary>
/// The bar line. /// The bar line.
@ -45,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Width = tracker_width; Width = tracker_width;
AddInternal(Tracker = new Box AddInternal(Line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box
{
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(0.5f, 0),
})
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, Alpha = 0.75f,
EdgeSmoothness = new Vector2(0.5f, 0),
Alpha = 0.75f
}); });
} }

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
} }
}); });
Tracker.Alpha = 1f; Line.Alpha = 1f;
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -1,21 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableCentreHit : DrawableHit
{
public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre };
public DrawableCentreHit(Hit hit)
: base(hit)
{
}
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit),
_ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
}
}

View File

@ -121,8 +121,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return; return;
int countHit = NestedHitObjects.Count(o => o.IsHit); int countHit = NestedHitObjects.Count(o => o.IsHit);
if (countHit >= HitObject.RequiredGoodHits) if (countHit >= HitObject.RequiredGoodHits)
{
ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good);
}
else else
ApplyResult(r => r.Type = HitResult.Miss); ApplyResult(r => r.Type = HitResult.Miss);
} }

View File

@ -12,14 +12,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
public class DrawableDrumRollTick : DrawableTaikoHitObject<DrumRollTick> public class DrawableDrumRollTick : DrawableTaikoHitObject<DrumRollTick>
{ {
/// <summary>
/// The hit type corresponding to the <see cref="TaikoAction"/> that the user pressed to hit this <see cref="DrawableDrumRollTick"/>.
/// </summary>
public HitType JudgementType;
public DrawableDrumRollTick(DrumRollTick tick) public DrawableDrumRollTick(DrumRollTick tick)
: base(tick) : base(tick)
{ {
FillMode = FillMode.Fit; FillMode = FillMode.Fit;
} }
public override bool DisplayResult => false;
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick), protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
_ => new TickPiece _ => new TickPiece
{ {
@ -51,7 +54,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
} }
} }
public override bool OnPressed(TaikoAction action) => UpdateResult(true); public override bool OnPressed(TaikoAction action)
{
JudgementType = action == TaikoAction.LeftRim || action == TaikoAction.RightRim ? HitType.Rim : HitType.Centre;
return UpdateResult(true);
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
/// <summary>
/// A hit used specifically for drum rolls, where spawning flying hits is required.
/// </summary>
public class DrawableFlyingHit : DrawableHit
{
public DrawableFlyingHit(DrawableDrumRollTick drumRollTick)
: base(new IgnoreHit
{
StartTime = drumRollTick.HitObject.StartTime + drumRollTick.Result.TimeOffset,
IsStrong = drumRollTick.HitObject.IsStrong,
Type = drumRollTick.JudgementType
})
{
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
}
protected override void LoadComplete()
{
base.LoadComplete();
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
}
}

View File

@ -8,31 +8,45 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
public abstract class DrawableHit : DrawableTaikoHitObject<Hit> public class DrawableHit : DrawableTaikoHitObject<Hit>
{ {
/// <summary> /// <summary>
/// A list of keys which can result in hits for this HitObject. /// A list of keys which can result in hits for this HitObject.
/// </summary> /// </summary>
public abstract TaikoAction[] HitActions { get; } public TaikoAction[] HitActions { get; }
/// <summary> /// <summary>
/// The action that caused this <see cref="DrawableHit"/> to be hit. /// The action that caused this <see cref="DrawableHit"/> to be hit.
/// </summary> /// </summary>
public TaikoAction? HitAction { get; private set; } public TaikoAction? HitAction
{
get;
private set;
}
private bool validActionPressed; private bool validActionPressed;
private bool pressHandledThisFrame; private bool pressHandledThisFrame;
protected DrawableHit(Hit hit) public DrawableHit(Hit hit)
: base(hit) : base(hit)
{ {
FillMode = FillMode.Fit; FillMode = FillMode.Fit;
HitActions =
HitObject.Type == HitType.Centre
? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
: new[] { TaikoAction.LeftRim, TaikoAction.RightRim };
} }
protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre
? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
: new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
Debug.Assert(HitObject.HitWindows != null); Debug.Assert(HitObject.HitWindows != null);
@ -58,7 +72,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
if (pressHandledThisFrame) if (pressHandledThisFrame)
return true; return true;
if (Judged) if (Judged)
return false; return false;
@ -66,14 +79,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// Only count this as handled if the new judgement is a hit // Only count this as handled if the new judgement is a hit
var result = UpdateResult(true); var result = UpdateResult(true);
if (IsHit) if (IsHit)
HitAction = action; HitAction = action;
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded // Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note // E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
pressHandledThisFrame = true; pressHandledThisFrame = true;
return result; return result;
} }
@ -81,7 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
if (action == HitAction) if (action == HitAction)
HitAction = null; HitAction = null;
base.OnReleased(action); base.OnReleased(action);
} }
@ -92,8 +102,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// The input manager processes all input prior to us updating, so this is the perfect time // The input manager processes all input prior to us updating, so this is the perfect time
// for us to remove the extra press blocking, before input is handled in the next frame // for us to remove the extra press blocking, before input is handled in the next frame
pressHandledThisFrame = false; pressHandledThisFrame = false;
Size = BaseSize * Parent.RelativeChildSize;
} }
protected override void UpdateStateTransforms(ArmedState state) protected override void UpdateStateTransforms(ArmedState state)

View File

@ -1,21 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableRimHit : DrawableHit
{
public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim };
public DrawableRimHit(Hit hit)
: base(hit)
{
}
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit),
_ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
}
}

View File

@ -3,15 +3,20 @@
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System; using System;
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Judgements;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
public class DrumRoll : TaikoHitObject, IHasEndTime public class DrumRoll : TaikoHitObject, IHasCurve
{ {
/// <summary> /// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length. /// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -26,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public double Duration { get; set; } public double Duration { get; set; }
/// <summary>
/// Velocity of this <see cref="DrumRoll"/>.
/// </summary>
public double Velocity { get; private set; }
/// <summary> /// <summary>
/// Numer of ticks per beat length. /// Numer of ticks per beat length.
/// </summary> /// </summary>
@ -54,6 +64,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate; tickSpacing = timingPoint.BeatLength / TickRate;
overallDifficulty = difficulty.OverallDifficulty; overallDifficulty = difficulty.OverallDifficulty;
@ -93,5 +107,18 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override Judgement CreateJudgement() => new TaikoDrumRollJudgement(); public override Judgement CreateJudgement() => new TaikoDrumRollJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
#region LegacyBeatmapEncoder
double IHasDistance.Distance => Duration * Velocity;
int IHasRepeats.RepeatCount { get => 0; set { } }
List<IList<HitSampleInfo>> IHasRepeats.NodeSamples => new List<IList<HitSampleInfo>>();
SliderPath IHasCurve.Path
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
#endregion
} }
} }

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class IgnoreHit : Hit
{
public override Judgement CreateJudgement() => new IgnoreJudgement();
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyBarLine : Sprite
{
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
Texture = skin.GetTexture("taiko-barline");
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(1, 0.88f);
FillMode = FillMode.Fill;
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyHitExplosion : CompositeDrawable
{
public LegacyHitExplosion(Drawable sprite)
{
InternalChild = sprite;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
}
protected override void LoadComplete()
{
base.LoadComplete();
const double animation_time = 120;
this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
this.ScaleTo(0.6f)
.Then().ScaleTo(1.1f, animation_time * 0.8)
.Then().ScaleTo(0.9f, animation_time * 0.4)
.Then().ScaleTo(1f, animation_time * 0.2);
Expire(true);
}
}
}

View File

@ -1,41 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyHitTarget : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Sprite
{
Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.73f),
Alpha = 0.7f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new Sprite
{
Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.7f),
Alpha = 0.5f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
}
}

View File

@ -20,15 +20,19 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{ {
private LegacyHalfDrum left; private LegacyHalfDrum left;
private LegacyHalfDrum right; private LegacyHalfDrum right;
private Container content;
public LegacyInputDrum() public LegacyInputDrum()
{ {
Size = new Vector2(180, 200); RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource skin)
{ {
Child = content = new Container
{
Size = new Vector2(180, 200),
Children = new Drawable[] Children = new Drawable[]
{ {
new Sprite new Sprite
@ -51,6 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
RimAction = TaikoAction.RightRim, RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre CentreAction = TaikoAction.RightCentre
} }
}
}; };
// this will be used in the future for stable skin alignment. keeping here for reference. // this will be used in the future for stable skin alignment. keeping here for reference.
@ -60,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
const float ratio = 1.6f; const float ratio = 1.6f;
// because the right half is flipped, we need to position using width - position to get the true "topleft" origin position // because the right half is flipped, we need to position using width - position to get the true "topleft" origin position
float negativeScaleAdjust = Width / ratio; float negativeScaleAdjust = content.Width / ratio;
if (skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m) if (skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m)
{ {
@ -78,6 +83,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning
} }
} }
protected override void Update()
{
base.Update();
// Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
// This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
content.Scale = new Vector2(DrawHeight / content.Size.Y);
}
/// <summary> /// <summary>
/// A half-drum. Contains one centre and one rim hit. /// A half-drum. Contains one centre and one rim hit.
/// </summary> /// </summary>

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class TaikoLegacyHitTarget : CompositeDrawable
{
private Container content;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
RelativeSizeAxes = Axes.Both;
InternalChild = content = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Sprite
{
Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.73f),
Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new Sprite
{
Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.7f),
Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
};
}
protected override void Update()
{
base.Update();
// Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements.
// This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement.
content.Scale = new Vector2(DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
}
}
}

View File

@ -0,0 +1,57 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class TaikoLegacyPlayfieldBackgroundRight : BeatSyncedContainer
{
private Sprite kiai;
private bool kiaiDisplayed;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Sprite
{
Texture = skin.GetTexture("taiko-bar-right"),
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
},
kiai = new Sprite
{
Texture = skin.GetTexture("taiko-bar-right-glow"),
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Alpha = 0,
}
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (effectPoint.KiaiMode != kiaiDisplayed)
{
kiaiDisplayed = effectPoint.KiaiMode;
kiai.ClearTransforms();
kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200);
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -52,7 +53,36 @@ namespace osu.Game.Rulesets.Taiko.Skinning
case TaikoSkinComponents.HitTarget: case TaikoSkinComponents.HitTarget:
if (GetTexture("taikobigcircle") != null) if (GetTexture("taikobigcircle") != null)
return new LegacyHitTarget(); return new TaikoLegacyHitTarget();
return null;
case TaikoSkinComponents.PlayfieldBackgroundRight:
if (GetTexture("taiko-bar-right") != null)
return new TaikoLegacyPlayfieldBackgroundRight();
return null;
case TaikoSkinComponents.PlayfieldBackgroundLeft:
// This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins).
if (GetTexture("taiko-bar-right") != null)
return Drawable.Empty();
return null;
case TaikoSkinComponents.BarLine:
if (GetTexture("taiko-barline") != null)
return new LegacyBarLine();
return null;
case TaikoSkinComponents.TaikoExplosionGood:
case TaikoSkinComponents.TaikoExplosionGreat:
case TaikoSkinComponents.TaikoExplosionMiss:
var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
if (sprite != null)
return new LegacyHitExplosion(sprite);
return null; return null;
} }
@ -60,6 +90,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return source.GetDrawableComponent(component); return source.GetDrawableComponent(component);
} }
private string getHitName(TaikoSkinComponents component)
{
switch (component)
{
case TaikoSkinComponents.TaikoExplosionMiss:
return "taiko-hit0";
case TaikoSkinComponents.TaikoExplosionGood:
return "taiko-hit100";
case TaikoSkinComponents.TaikoExplosionGreat:
return "taiko-hit300";
}
throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");
}
public Texture GetTexture(string componentName) => source.GetTexture(componentName); public Texture GetTexture(string componentName) => source.GetTexture(componentName);
public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));

View File

@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Taiko
DrumRollBody, DrumRollBody,
DrumRollTick, DrumRollTick,
Swell, Swell,
HitTarget HitTarget,
PlayfieldBackgroundLeft,
PlayfieldBackgroundRight,
BarLine,
TaikoExplosionMiss,
TaikoExplosionGood,
TaikoExplosionGreat,
} }
} }

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
internal class DefaultHitExplosion : CircularContainer
{
[Resolved]
private DrawableHitObject judgedObject { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
BorderColour = Color4.White;
BorderThickness = 1;
Blending = BlendingParameters.Additive;
Alpha = 0.15f;
Masking = true;
if (judgedObject.Result.Type == HitResult.Miss)
return;
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
InternalChildren = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = isRim ? colours.BlueDarker : colours.PinkDarker,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
this.ScaleTo(3f, 1000, Easing.OutQuint);
this.FadeOut(500);
Expire(true);
}
}
}

View File

@ -49,10 +49,7 @@ namespace osu.Game.Rulesets.Taiko.UI
switch (h) switch (h)
{ {
case Hit hit: case Hit hit:
if (hit.Type == HitType.Centre) return new DrawableHit(hit);
return new DrawableCentreHit(hit);
else
return new DrawableRimHit(hit);
case DrumRoll drumRoll: case DrumRoll drumRoll:
return new DrawableDrumRoll(drumRoll); return new DrawableDrumRoll(drumRoll);

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Taiko.UI
{
internal class DrumRollHitContainer : ScrollingHitObjectContainer
{
protected override void Update()
{
base.Update();
// Remove any auxiliary hit notes that were spawned during a drum roll but subsequently rewound.
for (var i = AliveInternalChildren.Count - 1; i >= 0; i--)
{
var flyingHit = (DrawableFlyingHit)AliveInternalChildren[i];
if (Time.Current <= flyingHit.HitObject.StartTime)
Remove(flyingHit);
}
}
protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
{
base.OnChildLifetimeBoundaryCrossed(e);
// ensure all old hits are removed on becoming alive (may miss being in the AliveInternalChildren list above).
if (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward)
Remove((DrawableHitObject)e.Child);
}
}
}

View File

@ -1,15 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osuTK; using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
{ {
@ -20,55 +20,49 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public override bool RemoveWhenNotAlive => true; public override bool RemoveWhenNotAlive => true;
[Cached(typeof(DrawableHitObject))]
public readonly DrawableHitObject JudgedObject; public readonly DrawableHitObject JudgedObject;
private readonly Box innerFill; private SkinnableDrawable skinnable;
private readonly bool isRim; public override double LifetimeStart => skinnable.Drawable.LifetimeStart;
public HitExplosion(DrawableHitObject judgedObject, bool isRim) public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd;
public HitExplosion(DrawableHitObject judgedObject)
{ {
this.isRim = isRim;
JudgedObject = judgedObject; JudgedObject = judgedObject;
Anchor = Anchor.CentreLeft; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE); Size = new Vector2(TaikoHitObject.DEFAULT_SIZE);
RelativePositionAxes = Axes.Both; RelativePositionAxes = Axes.Both;
BorderColour = Color4.White;
BorderThickness = 1;
Alpha = 0.15f;
Masking = true;
Children = new[]
{
innerFill = new Box
{
RelativeSizeAxes = Axes.Both,
}
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
innerFill.Colour = isRim ? colours.BlueDarker : colours.PinkDarker; Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion());
} }
protected override void LoadComplete() private TaikoSkinComponents getComponentName(HitResult resultType)
{ {
base.LoadComplete(); switch (resultType)
{
case HitResult.Miss:
return TaikoSkinComponents.TaikoExplosionMiss;
this.ScaleTo(3f, 1000, Easing.OutQuint); case HitResult.Good:
this.FadeOut(500); return TaikoSkinComponents.TaikoExplosionGood;
Expire(true); case HitResult.Great:
return TaikoSkinComponents.TaikoExplosionGreat;
}
throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type");
} }
/// <summary> /// <summary>

View File

@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Taiko.UI
sampleMapping = new DrumSampleMapping(controlPoints); sampleMapping = new DrumSampleMapping(controlPoints);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -40,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.UI
Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Scale = new Vector2(0.9f),
Children = new Drawable[] Children = new Drawable[]
{ {
new TaikoHalfDrum(false) new TaikoHalfDrum(false)

View File

@ -18,14 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
public override bool RemoveWhenNotAlive => true; public override bool RemoveWhenNotAlive => true;
public readonly DrawableHitObject JudgedObject; public readonly DrawableHitObject JudgedObject;
private readonly HitType type;
private readonly bool isRim; public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type)
public KiaiHitExplosion(DrawableHitObject judgedObject, bool isRim)
{ {
this.isRim = isRim;
JudgedObject = judgedObject; JudgedObject = judgedObject;
this.type = type;
Anchor = Anchor.CentreLeft; Anchor = Anchor.CentreLeft;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -33,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1);
Blending = BlendingParameters.Additive;
Masking = true; Masking = true;
Alpha = 0.25f; Alpha = 0.25f;
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.UI
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Colour = isRim ? colours.BlueDarker : colours.PinkDarker, Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker,
Radius = 60, Radius = 60,
}; };
} }

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
internal class PlayfieldBackgroundLeft : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray1,
RelativeSizeAxes = Axes.Both,
},
new Box
{
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = 10,
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
},
};
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
public class PlayfieldBackgroundRight : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Name = "Transparent playfield background";
RelativeSizeAxes = Axes.Both;
Masking = true;
BorderColour = colours.Gray1;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.2f),
Radius = 5,
};
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray0,
Alpha = 0.6f
},
new Container
{
Name = "Border",
RelativeSizeAxes = Axes.Both,
Masking = true,
MaskingSmoothness = 0,
BorderThickness = 2,
AlwaysPresent = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
};
}
}
}

View File

@ -13,14 +13,14 @@ namespace osu.Game.Rulesets.Taiko.UI
/// <summary> /// <summary>
/// A component that is displayed at the hit position in the taiko playfield. /// A component that is displayed at the hit position in the taiko playfield.
/// </summary> /// </summary>
internal class HitTarget : Container internal class TaikoHitTarget : Container
{ {
/// <summary> /// <summary>
/// Thickness of all drawn line pieces. /// Thickness of all drawn line pieces.
/// </summary> /// </summary>
private const float border_thickness = 2.5f; private const float border_thickness = 2.5f;
public HitTarget() public TaikoHitTarget()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
Masking = true, Masking = true,
BorderColour = Color4.White, BorderColour = Color4.White,
@ -63,7 +62,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE), Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE),
Masking = true, Masking = true,
BorderColour = Color4.White, BorderColour = Color4.White,

View File

@ -3,11 +3,8 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -18,193 +15,138 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
{ {
public class TaikoPlayfield : ScrollingPlayfield public class TaikoPlayfield : ScrollingPlayfield
{ {
private readonly ControlPointInfo controlPoints;
/// <summary> /// <summary>
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>. /// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
/// </summary> /// </summary>
public const float DEFAULT_HEIGHT = 178; public const float DEFAULT_HEIGHT = 178;
/// <summary> private Container<HitExplosion> hitExplosionContainer;
/// The offset from <see cref="left_area_size"/> which the center of the hit target lies at. private Container<KiaiHitExplosion> kiaiExplosionContainer;
/// </summary> private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
public const float HIT_TARGET_OFFSET = 100; private ScrollingHitObjectContainer drumRollHitContainer;
internal Drawable HitTarget;
/// <summary> private ProxyContainer topLevelHitContainer;
/// The size of the left area of the playfield. This area contains the input drum. private ProxyContainer barlineContainer;
/// </summary> private Container rightArea;
private const float left_area_size = 240; private Container leftArea;
private readonly Container<HitExplosion> hitExplosionContainer; private Container hitTargetOffsetContent;
private readonly Container<KiaiHitExplosion> kiaiExplosionContainer;
private readonly JudgementContainer<DrawableTaikoJudgement> judgementContainer;
internal readonly Drawable HitTarget;
private readonly ProxyContainer topLevelHitContainer;
private readonly ProxyContainer barlineContainer;
private readonly Container overlayBackgroundContainer;
private readonly Container backgroundContainer;
private readonly Box overlayBackground;
private readonly Box background;
public TaikoPlayfield(ControlPointInfo controlPoints) public TaikoPlayfield(ControlPointInfo controlPoints)
{ {
InternalChildren = new Drawable[] this.controlPoints = controlPoints;
{
backgroundContainer = new Container
{
Name = "Transparent playfield background",
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.2f),
Radius = 5,
},
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.6f
},
} }
},
new Container [BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new[]
{
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()),
rightArea = new Container
{ {
Name = "Right area", Name = "Right area",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = left_area_size }, RelativePositionAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
new Container new Container
{ {
Name = "Masked elements before hit objects", Name = "Masked elements before hit objects",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, FillMode = FillMode.Fit,
Masking = true,
Children = new[] Children = new[]
{ {
hitExplosionContainer = new Container<HitExplosion> hitExplosionContainer = new Container<HitExplosion>
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Blending = BlendingParameters.Additive,
}, },
HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new HitTarget()) HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget())
{ {
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit
} }
} }
}, },
hitTargetOffsetContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
barlineContainer = new ProxyContainer barlineContainer = new ProxyContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
}, },
new Container new Container
{ {
Name = "Hit objects", Name = "Hit objects",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, Children = new Drawable[]
Masking = true, {
Child = HitObjectContainer HitObjectContainer,
drumRollHitContainer = new DrumRollHitContainer()
}
}, },
kiaiExplosionContainer = new Container<KiaiHitExplosion> kiaiExplosionContainer = new Container<KiaiHitExplosion>
{ {
Name = "Kiai hit explosions", Name = "Kiai hit explosions",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingParameters.Additive
}, },
judgementContainer = new JudgementContainer<DrawableTaikoJudgement> judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
{ {
Name = "Judgements", Name = "Judgements",
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingParameters.Additive
}, },
} }
}, },
overlayBackgroundContainer = new Container }
},
leftArea = new Container
{ {
Name = "Left overlay", Name = "Left overlay",
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Both,
Size = new Vector2(left_area_size, 1), FillMode = FillMode.Fit,
BorderColour = colours.Gray0,
Children = new Drawable[] Children = new Drawable[]
{ {
overlayBackground = new Box new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
{
RelativeSizeAxes = Axes.Both,
},
new InputDrum(controlPoints) new InputDrum(controlPoints)
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight, Origin = Anchor.CentreLeft,
Scale = new Vector2(0.9f),
Margin = new MarginPadding { Right = 20 }
}, },
new Box
{
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = 10,
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
},
}
},
new Container
{
Name = "Border",
RelativeSizeAxes = Axes.Both,
Masking = true,
MaskingSmoothness = 0,
BorderThickness = 2,
AlwaysPresent = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
} }
}, },
topLevelHitContainer = new ProxyContainer topLevelHitContainer = new ProxyContainer
{ {
Name = "Top level hit objects", Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
} },
drumRollHitContainer.CreateProxy()
}; };
} }
[BackgroundDependencyLoader] protected override void Update()
private void load(OsuColour colours)
{ {
overlayBackgroundContainer.BorderColour = colours.Gray0; base.Update();
overlayBackground.Colour = colours.Gray1;
backgroundContainer.BorderColour = colours.Gray1; // Padding is required to be updated for elements which are based on "absolute" X sized elements.
background.Colour = colours.Gray0; // This is basically allowing for correct alignment as relative pieces move around them.
rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
} }
public override void Add(DrawableHitObject h) public override void Add(DrawableHitObject h)
{ {
h.OnNewResult += OnNewResult; h.OnNewResult += OnNewResult;
base.Add(h); base.Add(h);
switch (h) switch (h)
@ -223,7 +165,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
if (!DisplayJudgements.Value) if (!DisplayJudgements.Value)
return; return;
if (!judgedObject.DisplayResult) if (!judgedObject.DisplayResult)
return; return;
@ -234,6 +175,15 @@ namespace osu.Game.Rulesets.Taiko.UI
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit();
break; break;
case TaikoDrumRollTickJudgement _:
if (!result.IsHit)
break;
var drawableTick = (DrawableDrumRollTick)judgedObject;
addDrumRollHit(drawableTick);
break;
default: default:
judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject) judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject)
{ {
@ -246,17 +196,23 @@ namespace osu.Game.Rulesets.Taiko.UI
if (!result.IsHit) if (!result.IsHit)
break; break;
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim));
if (judgedObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim));
addExplosion(judgedObject, type);
break; break;
} }
} }
private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
private void addExplosion(DrawableHitObject drawableObject, HitType type)
{
hitExplosionContainer.Add(new HitExplosion(drawableObject));
if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
}
private class ProxyContainer : LifetimeManagementContainer private class ProxyContainer : LifetimeManagementContainer
{ {
public new MarginPadding Padding public new MarginPadding Padding

View File

@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore.Internal;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -137,7 +137,7 @@ namespace osu.Game.Tests.Beatmaps
var hitCircle = new HitCircle { StartTime = 1000 }; var hitCircle = new HitCircle { StartTime = 1000 };
editorBeatmap.Add(hitCircle); editorBeatmap.Add(hitCircle);
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1)); Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(3)); Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(3));
} }
/// <summary> /// <summary>
@ -161,7 +161,7 @@ namespace osu.Game.Tests.Beatmaps
hitCircle.StartTime = 0; hitCircle.StartTime = 0;
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1)); Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1)); Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(1));
} }
/// <summary> /// <summary>

View File

@ -5,7 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editor namespace osu.Game.Tests.Editing
{ {
[TestFixture] [TestFixture]
public class EditorChangeHandlerTest public class EditorChangeHandlerTest

View File

@ -17,7 +17,7 @@ using osu.Game.Screens.Edit;
using osuTK; using osuTK;
using Decoder = osu.Game.Beatmaps.Formats.Decoder; using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Tests.Editor namespace osu.Game.Tests.Editing
{ {
[TestFixture] [TestFixture]
public class LegacyEditorBeatmapPatcherTest public class LegacyEditorBeatmapPatcherTest
@ -304,6 +304,31 @@ namespace osu.Game.Tests.Editor
runTest(patch); runTest(patch);
} }
[Test]
public void TestChangeHitObjectAtSameTime()
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500, Position = new Vector2(50) },
new HitCircle { StartTime = 500, Position = new Vector2(100) },
new HitCircle { StartTime = 500, Position = new Vector2(150) },
new HitCircle { StartTime = 500, Position = new Vector2(200) },
});
var patch = new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 500, Position = new Vector2(150) },
new HitCircle { StartTime = 500, Position = new Vector2(100) },
new HitCircle { StartTime = 500, Position = new Vector2(50) },
new HitCircle { StartTime = 500, Position = new Vector2(200) },
}
};
runTest(patch);
}
private void runTest(IBeatmap patch) private void runTest(IBeatmap patch)
{ {
// Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder. // Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder.

View File

@ -14,7 +14,7 @@ using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Editor namespace osu.Game.Tests.Editing
{ {
[HeadlessTest] [HeadlessTest]
public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Utils;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class PeriodTrackerTest
{
private static readonly Period[] single_period = { new Period(1.0, 2.0) };
private static readonly Period[] unordered_periods =
{
new Period(-9.1, -8.3),
new Period(-3.4, 2.1),
new Period(9.0, 50.0),
new Period(5.25, 10.50)
};
[Test]
public void TestCheckValueInsideSinglePeriod()
{
var tracker = new PeriodTracker(single_period);
var period = single_period.Single();
Assert.IsTrue(tracker.IsInAny(period.Start));
Assert.IsTrue(tracker.IsInAny(getMidpoint(period)));
Assert.IsTrue(tracker.IsInAny(period.End));
}
[Test]
public void TestCheckValuesInsidePeriods()
{
var tracker = new PeriodTracker(unordered_periods);
foreach (var period in unordered_periods)
Assert.IsTrue(tracker.IsInAny(getMidpoint(period)));
}
[Test]
public void TestCheckValuesInRandomOrder()
{
var tracker = new PeriodTracker(unordered_periods);
foreach (var period in unordered_periods.OrderBy(_ => RNG.Next()))
Assert.IsTrue(tracker.IsInAny(getMidpoint(period)));
}
[Test]
public void TestCheckValuesOutOfPeriods()
{
var tracker = new PeriodTracker(new[]
{
new Period(1.0, 2.0),
new Period(3.0, 4.0)
});
Assert.IsFalse(tracker.IsInAny(0.9), "Time before first period is being considered inside");
Assert.IsFalse(tracker.IsInAny(2.1), "Time right after first period is being considered inside");
Assert.IsFalse(tracker.IsInAny(2.9), "Time right before second period is being considered inside");
Assert.IsFalse(tracker.IsInAny(4.1), "Time after last period is being considered inside");
}
[Test]
public void TestReversedPeriodHandling()
{
Assert.Throws<ArgumentException>(() =>
{
_ = new PeriodTracker(new[]
{
new Period(2.0, 1.0)
});
});
}
private double getMidpoint(Period period) => period.Start + (period.End - period.Start) / 2;
}
}

View File

@ -0,0 +1,30 @@
osu file format v14
[General]
SampleSet: Normal
StackLeniency: 0.7
Mode: 2
[Difficulty]
HPDrainRate:3
CircleSize:5
OverallDifficulty:8
ApproachRate:8
SliderMultiplier:3.59999990463257
SliderTickRate:2
[TimingPoints]
24,352.941176470588,4,1,1,100,1,0
6376,-50,4,1,1,100,0,0
[HitObjects]
32,183,24,5,0,0:0:0:0:
106,123,200,1,10,0:0:0:0:
199,108,376,1,2,0:0:0:0:
305,105,553,5,4,0:0:0:0:
386,112,729,1,14,0:0:0:0:
486,197,906,5,12,0:0:0:0:
14,199,1082,2,0,L|473:198,1,449.999988079071
14,199,1700,6,6,P|248:33|490:222,1,629.9999833107,0|8,0:0|0:0,0:0:0:0:
10,190,2494,2,8,B|252:29|254:335|468:167,1,449.999988079071,10|12,0:0|0:0,0:0:0:0:
256,192,3112,12,0,3906,0:0:0:0:

View File

@ -0,0 +1,39 @@
osu file format v14
[General]
SampleSet: Normal
StackLeniency: 0.7
Mode: 3
[Difficulty]
HPDrainRate:3
CircleSize:5
OverallDifficulty:8
ApproachRate:8
SliderMultiplier:3.59999990463257
SliderTickRate:2
[TimingPoints]
24,352.941176470588,4,1,1,100,1,0
6376,-50,4,1,1,100,0,0
[HitObjects]
51,192,24,1,0,0:0:0:0:
153,192,200,1,0,0:0:0:0:
358,192,376,1,0,0:0:0:0:
460,192,553,1,0,0:0:0:0:
460,192,729,128,0,1435:0:0:0:0:
358,192,906,128,0,1612:0:0:0:0:
256,192,1082,128,0,1788:0:0:0:0:
153,192,1259,128,0,1965:0:0:0:0:
51,192,1435,128,0,2141:0:0:0:0:
51,192,2318,1,12,0:0:0:0:
153,192,2318,1,4,0:0:0:0:
256,192,2318,1,6,0:0:0:0:
358,192,2318,1,14,0:0:0:0:
460,192,2318,1,0,0:0:0:0:
51,192,2494,128,0,2582:0:0:0:0:
153,192,2494,128,14,2582:0:0:0:0:
256,192,2494,128,6,2582:0:0:0:0:
358,192,2494,128,4,2582:0:0:0:0:
460,192,2494,128,12,2582:0:0:0:0:

View File

@ -0,0 +1,42 @@
osu file format v14
[General]
SampleSet: Normal
StackLeniency: 0.7
Mode: 1
[Difficulty]
HPDrainRate:3
CircleSize:5
OverallDifficulty:8
ApproachRate:8
SliderMultiplier:3.59999990463257
SliderTickRate:2
[TimingPoints]
24,352.941176470588,4,1,1,100,1,0
6376,-50,4,1,1,100,0,0
[HitObjects]
231,129,24,1,0,0:0:0:0:
231,129,200,1,0,0:0:0:0:
231,129,376,1,0,0:0:0:0:
231,129,553,1,0,0:0:0:0:
231,129,729,1,0,0:0:0:0:
373,132,906,1,4,0:0:0:0:
373,132,1082,1,4,0:0:0:0:
373,132,1259,1,4,0:0:0:0:
373,132,1435,1,4,0:0:0:0:
231,129,1788,1,8,0:0:0:0:
231,129,1964,1,8,0:0:0:0:
231,129,2140,1,8,0:0:0:0:
231,129,2317,1,8,0:0:0:0:
231,129,2493,1,8,0:0:0:0:
373,132,2670,1,12,0:0:0:0:
373,132,2846,1,12,0:0:0:0:
373,132,3023,1,12,0:0:0:0:
373,132,3199,1,12,0:0:0:0:
51,189,3553,2,0,L|150:188,1,89.9999976158143
52,191,3906,2,0,L|512:189,1,449.999988079071
26,196,4612,2,4,L|501:195,1,449.999988079071
17,242,5318,2,10,P|250:69|495:243,1,629.9999833107,0|8,0:0|0:0,0:0:0:0:

View File

@ -14,7 +14,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{ {

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene public class TestSceneComposeScreen : EditorClockTestScene

View File

@ -13,7 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
public class TestSceneDistanceSnapGrid : EditorClockTestScene public class TestSceneDistanceSnapGrid : EditorClockTestScene
{ {

View File

@ -4,33 +4,27 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
public class TestSceneEditorChangeStates : ScreenTestScene public class TestSceneEditorChangeStates : EditorTestScene
{ {
public TestSceneEditorChangeStates()
: base(new OsuRuleset())
{
}
private EditorBeatmap editorBeatmap; private EditorBeatmap editorBeatmap;
private TestEditor editor;
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("load editor", () => AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
LoadScreen(editor = new TestEditor());
});
AddUntilStep("wait for editor to load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single());
} }
[Test] [Test]
@ -158,11 +152,13 @@ namespace osu.Game.Tests.Visual.Editor
AddAssert("no hitobject added", () => addedObject == null); AddAssert("no hitobject added", () => addedObject == null);
} }
private void addUndoSteps() => AddStep("undo", () => editor.Undo()); private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo());
private void addRedoSteps() => AddStep("redo", () => editor.Redo()); private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo());
private class TestEditor : Screens.Edit.Editor protected override Editor CreateEditor() => new TestEditor();
private class TestEditor : Editor
{ {
public new void Undo() => base.Undo(); public new void Undo() => base.Undo();

View File

@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneEditorComposeRadioButtons : OsuTestScene public class TestSceneEditorComposeRadioButtons : OsuTestScene

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneEditorMenuBar : OsuTestScene public class TestSceneEditorMenuBar : OsuTestScene

View File

@ -13,7 +13,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneEditorSeekSnapping : EditorClockTestScene public class TestSceneEditorSeekSnapping : EditorClockTestScene

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneEditorSummaryTimeline : EditorClockTestScene public class TestSceneEditorSummaryTimeline : EditorClockTestScene

View File

@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneHitObjectComposer : EditorClockTestScene public class TestSceneHitObjectComposer : EditorClockTestScene

View File

@ -9,7 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestScenePlaybackControl : OsuTestScene public class TestScenePlaybackControl : OsuTestScene

View File

@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneTimelineBlueprintContainer : TimelineTestScene public class TestSceneTimelineBlueprintContainer : TimelineTestScene

View File

@ -8,7 +8,7 @@ using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneTimelineTickDisplay : TimelineTestScene public class TestSceneTimelineTickDisplay : TimelineTestScene

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneTimingScreen : EditorClockTestScene public class TestSceneTimingScreen : EditorClockTestScene

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