diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 082e0d247c..56b3ebe87b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,6 +4,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
inspect-code:
name: Code Quality
@@ -85,7 +88,7 @@ jobs:
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
- run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
+ run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
shell: pwsh
# Attempt to upload results even if test fails.
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index 358cbda17a..bfc9620174 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -8,8 +8,12 @@ on:
workflows: ["Continuous Integration"]
types:
- completed
+permissions: {}
jobs:
annotate:
+ permissions:
+ checks: write # to create checks (dorny/test-reporter)
+
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml
index 442b97c473..cce3f23e5f 100644
--- a/.github/workflows/sentry-release.yml
+++ b/.github/workflows/sentry-release.yml
@@ -5,6 +5,9 @@ on:
tags:
- '*'
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
sentry_release:
runs-on: ubuntu-latest
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index b8604169aa..936808f38b 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 4117452579..35e7742172 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 0b119c8680..c1044965b5 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 4117452579..35e7742172 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/osu.Android.props b/osu.Android.props
index 77c29a5d6e..2a678f1c61 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
-
+
+
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index d9ad95f96a..3ee1b3da30 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -137,12 +137,13 @@ namespace osu.Desktop
{
base.SetHost(host);
- var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
-
var desktopWindow = (SDL2DesktopWindow)host.Window;
+ var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
+ if (iconStream != null)
+ desktopWindow.SetIconFromStream(iconStream);
+
desktopWindow.CursorState |= CursorState.Hidden;
- desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 36ffd3b5b6..d62d422f33 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,9 +7,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
index 6ecbf58a52..a4b2b26624 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
});
}
- private class TestSkin : DefaultSkin
+ private class TestSkin : TrianglesSkin
{
public bool FlipCatcherPlate { get; set; }
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
index cbf6e8f202..cf6a8169c4 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
@@ -1,10 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
@@ -12,11 +17,11 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture]
public class TestSceneCatchTouchInput : OsuTestScene
{
- private CatchTouchInputMapper catchTouchInputMapper = null!;
-
- [SetUpSteps]
- public void SetUpSteps()
+ [Test]
+ public void TestBasic()
{
+ CatchTouchInputMapper catchTouchInputMapper = null!;
+
AddStep("create input overlay", () =>
{
Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
@@ -32,12 +37,30 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
});
+
+ AddStep("show overlay", () => catchTouchInputMapper.Show());
}
[Test]
- public void TestBasic()
+ public void TestWithoutRelax()
{
- AddStep("show overlay", () => catchTouchInputMapper.Show());
+ AddStep("create drawable ruleset without relax mod", () =>
+ {
+ Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List());
+ });
+ AddUntilStep("wait for load", () => Child.IsLoaded);
+ AddAssert("check touch input is shown", () => this.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestWithRelax()
+ {
+ AddStep("create drawable ruleset with relax mod", () =>
+ {
+ Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List { new CatchModRelax() });
+ });
+ AddUntilStep("wait for load", () => Child.IsLoaded);
+ AddAssert("check touch input is not shown", () => !this.ChildrenOfType().Any());
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 956d0e0c14..2dc99077d3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -21,7 +21,6 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@@ -106,20 +105,37 @@ namespace osu.Game.Rulesets.Catch.Tests
public void TestCatcherCatchWidth()
{
float halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
+
+ AddStep("move catcher to center", () => catcher.X = CatchPlayfield.CENTER_X);
+
+ float leftPlateBounds = CatchPlayfield.CENTER_X - halfWidth;
+ float rightPlateBounds = CatchPlayfield.CENTER_X + halfWidth;
+
AddStep("catch fruit", () =>
{
- attemptCatch(new Fruit { X = -halfWidth + 1 });
- attemptCatch(new Fruit { X = halfWidth - 1 });
+ attemptCatch(new Fruit { X = leftPlateBounds + 1 });
+ attemptCatch(new Fruit { X = rightPlateBounds - 1 });
});
checkPlate(2);
+
AddStep("miss fruit", () =>
{
- attemptCatch(new Fruit { X = -halfWidth - 1 });
- attemptCatch(new Fruit { X = halfWidth + 1 });
+ attemptCatch(new Fruit { X = leftPlateBounds - 1 });
+ attemptCatch(new Fruit { X = rightPlateBounds + 1 });
});
checkPlate(2);
}
+ [Test]
+ public void TestFruitClampedToCatchableRegion()
+ {
+ AddStep("catch fruit left", () => attemptCatch(new Fruit { X = -CatchPlayfield.WIDTH }));
+ checkPlate(1);
+ AddStep("move catcher to right", () => catcher.X = CatchPlayfield.WIDTH);
+ AddStep("catch fruit right", () => attemptCatch(new Fruit { X = CatchPlayfield.WIDTH * 2 }));
+ checkPlate(2);
+ }
+
[Test]
public void TestFruitChangesCatcherState()
{
@@ -233,11 +249,9 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestHitLightingColour()
{
- var fruitColour = SkinConfiguration.DefaultComboColours[1];
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
- AddAssert("correct hit lighting colour", () =>
- catcher.ChildrenOfType().First()?.Entry?.ObjectColour == fruitColour);
+ AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType().First()?.Entry?.ObjectColour == this.ChildrenOfType().First().AccentColour.Value);
}
[Test]
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index d45f8a9692..c9db824615 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index abe391ba4e..ff957b9b73 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@@ -16,24 +15,16 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
- [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableFloat SizeMultiplier { get; } = new BindableFloat
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 1.5f,
- Default = 1f,
- Value = 1f,
Precision = 0.1f
};
- [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
- public override BindableBool ComboBasedSize { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
- public override float DefaultFlashlightSize => 350;
+ public override float DefaultFlashlightSize => 325;
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);
@@ -53,7 +44,19 @@ namespace osu.Game.Rulesets.Catch.Mods
: base(modFlashlight)
{
this.playfield = playfield;
+
FlashlightSize = new Vector2(0, GetSizeFor(0));
+ FlashlightSmoothness = 1.4f;
+ }
+
+ protected override float GetComboScaleFor(int combo)
+ {
+ if (combo >= 200)
+ return 0.770f;
+ if (combo >= 100)
+ return 0.885f;
+
+ return 1.0f;
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
index 9038153e20..19b4a39f97 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
@@ -6,8 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Framework.Utils;
-using osu.Game.Configuration;
-using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
@@ -17,15 +15,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override LocalisableString Description => "Where's the catcher?";
- [SettingSource(
- "Hidden at combo",
- "The combo count at which the catcher becomes completely hidden",
- SettingControlType = typeof(SettingsSlider)
- )]
- public override BindableInt HiddenComboCount { get; } = new BindableInt
+ public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
- Default = 10,
- Value = 10,
MinValue = 0,
MaxValue = 50,
};
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index 6e01c44e1f..cd2b8348e2 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value plus the offset applied by the beatmap processing.
/// Use if a value not affected by the offset is desired.
///
- public float EffectiveX => OriginalX + XOffset;
+ public float EffectiveX => Math.Clamp(OriginalX + XOffset, 0, CatchPlayfield.WIDTH);
public double TimePreempt { get; set; } = 1000;
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 311e15116e..015457e84f 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -11,6 +11,7 @@ using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -84,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
- X = OriginalX + Path.PositionAt(
- lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
+ X = ClampToPlayfield(EffectiveX + Path.PositionAt(
+ lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X),
});
}
}
@@ -102,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
- X = OriginalX + Path.PositionAt(e.PathProgress).X,
+ X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
});
break;
@@ -113,14 +114,16 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
- X = OriginalX + Path.PositionAt(e.PathProgress).X,
+ X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
});
break;
}
}
}
- public float EndX => OriginalX + this.CurvePositionAt(1).X;
+ public float EndX => ClampToPlayfield(EffectiveX + this.CurvePositionAt(1).X);
+
+ public float ClampToPlayfield(float value) => Math.Clamp(value, 0, CatchPlayfield.WIDTH);
[JsonIgnore]
public double Duration
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
index 0630de9156..8f46bdbe6e 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(SkinManager skins)
{
- var defaultLegacySkin = skins.DefaultLegacySkin;
+ var defaultLegacySkin = skins.DefaultClassicSkin;
// sprite names intentionally swapped to match stable member naming / ease of cross-referencing
explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index ef2936ac94..27f7886d79 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Game.Beatmaps;
@@ -36,7 +37,9 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader]
private void load()
{
- KeyBindingInputManager.Add(new CatchTouchInputMapper());
+ // With relax mod, input maps directly to x position and left/right buttons are not used.
+ if (!Mods.Any(m => m is ModRelax))
+ KeyBindingInputManager.Add(new CatchTouchInputMapper());
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
index 6e6e83f9cf..0e4f612999 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@@ -30,15 +31,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Cached(typeof(IScrollingInfo))]
private IScrollingInfo scrollingInfo;
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(5);
+
protected ManiaPlacementBlueprintTestScene()
{
scrollingInfo = ((ScrollingTestContainer)HitObjectContainer).ScrollingInfo;
- Add(column = new Column(0)
+ Add(column = new Column(0, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- AccentColour = Color4.OrangeRed,
+ AccentColour = { Value = Color4.OrangeRed },
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
index 679a15e8cb..4cadcf138b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
protected ManiaSelectionBlueprintTestScene(int columns)
{
- var stageDefinitions = new List { new StageDefinition { Columns = columns } };
+ var stageDefinitions = new List { new StageDefinition(columns) };
base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up)
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index ec96205067..ef140995ec 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
[Cached(typeof(EditorBeatmap))]
- private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition())
+ private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition(2))
{
BeatmapInfo =
{
@@ -56,8 +56,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
Playfield = new ManiaPlayfield(new List
{
- new StageDefinition { Columns = 4 },
- new StageDefinition { Columns = 3 }
+ new StageDefinition(4),
+ new StageDefinition(3)
})
{
Clock = new FramedClock(new StopwatchClock())
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index 0354228cca..e082b90d3b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
AddStep("setup compose screen", () =>
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ var beatmap = new ManiaBeatmap(new StageDefinition(4))
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
};
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestDefaultSkin()
{
- AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
+ AddStep("set default skin", () => skins.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
}
[Test]
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index fcc9e2e6c3..a3985be936 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
InternalChildren = new Drawable[]
{
- EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
}),
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs
deleted file mode 100644
index e53deb5269..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System.Collections.Generic;
-using osu.Game.Rulesets.Mania.Beatmaps;
-using NUnit.Framework;
-
-namespace osu.Game.Rulesets.Mania.Tests
-{
- [TestFixture]
- public class ManiaColumnTypeTest
- {
- [TestCase(new[]
- {
- ColumnType.Special
- }, 1)]
- [TestCase(new[]
- {
- ColumnType.Odd,
- ColumnType.Even,
- ColumnType.Even,
- ColumnType.Odd
- }, 4)]
- [TestCase(new[]
- {
- ColumnType.Odd,
- ColumnType.Even,
- ColumnType.Odd,
- ColumnType.Special,
- ColumnType.Odd,
- ColumnType.Even,
- ColumnType.Odd
- }, 7)]
- public void Test(IEnumerable expected, int columns)
- {
- var definition = new StageDefinition
- {
- Columns = columns
- };
- var results = getResults(definition);
- Assert.AreEqual(expected, results);
- }
-
- private IEnumerable getResults(StageDefinition definition)
- {
- for (int i = 0; i < definition.Columns; i++)
- yield return definition.GetTypeOfColumn(i);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
index b64006316e..7d1a934456 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(ManiaAction.Key8)]
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(9));
var frame = new ManiaReplayFrame(0, actions);
var legacyFrame = frame.ToLegacy(beatmap);
@@ -38,8 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(ManiaAction.Key8)]
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
- beatmap.Stages.Add(new StageDefinition { Columns = 5 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(5));
+ beatmap.Stages.Add(new StageDefinition(5));
var frame = new ManiaReplayFrame(0, actions);
var legacyFrame = frame.ToLegacy(beatmap);
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs
new file mode 100644
index 0000000000..3bd654e75e
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable disable
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using NUnit.Framework;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaSpecialColumnTest
+ {
+ [TestCase(new[]
+ {
+ true
+ }, 1)]
+ [TestCase(new[]
+ {
+ false,
+ false,
+ false,
+ false
+ }, 4)]
+ [TestCase(new[]
+ {
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false
+ }, 7)]
+ public void Test(IEnumerable special, int columns)
+ {
+ var definition = new StageDefinition(columns);
+ var results = getResults(definition);
+ Assert.AreEqual(special, results);
+ }
+
+ private IEnumerable getResults(StageDefinition definition)
+ {
+ for (int i = 0; i < definition.Columns; i++)
+ yield return definition.IsSpecialColumn(i);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
index 7970d5b594..d27a79c41d 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
private static ManiaBeatmap createRawBeatmap()
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(1));
beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 }); // Set BPM to 60
// Add test hit objects
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
index 1a3513d46c..d3e90170b2 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
@@ -24,15 +23,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached]
private readonly Column column;
+ [Cached]
+ private readonly StageDefinition stageDefinition = new StageDefinition(5);
+
public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false)
{
InternalChildren = new[]
{
- this.column = new Column(column)
+ this.column = new Column(column, false)
{
Action = { Value = action },
- AccentColour = Color4.Orange,
- ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd,
Alpha = showColumn ? 1 : 0
},
content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index fd82041ad8..75175c43d8 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -61,7 +61,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
c.Add(CreateHitObject().With(h =>
{
h.HitObject.StartTime = Time.Current + 5000;
- h.AccentColour.Value = Color4.Orange;
}));
})
},
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
index 9f235689b4..2c5535a65f 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
using osu.Game.Tests.Visual;
@@ -24,6 +25,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(4);
+
protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset();
protected ManiaSkinnableTestScene()
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
index ff557638a9..1bfe55b074 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
var stageDefinitions = new List
{
- new StageDefinition { Columns = 4 },
+ new StageDefinition(4),
};
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
deleted file mode 100644
index bbbd7edb7b..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Mania.UI.Components;
-using osu.Game.Skinning;
-using osuTK;
-
-namespace osu.Game.Rulesets.Mania.Tests.Skinning
-{
- public class TestSceneKeyArea : ManiaSkinnableTestScene
- {
- [BackgroundDependencyLoader]
- private void load()
- {
- SetContents(_ => new FillFlowContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.8f),
- Direction = FillDirection.Horizontal,
- Children = new Drawable[]
- {
- new ColumnTestContainer(0, ManiaAction.Key1)
- {
- RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
- {
- RelativeSizeAxes = Axes.Both
- },
- },
- new ColumnTestContainer(1, ManiaAction.Key2)
- {
- RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
- {
- RelativeSizeAxes = Axes.Both
- },
- },
- }
- });
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
index 62dadbc3dd..9817719c94 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
stageDefinitions = new List
{
- new StageDefinition { Columns = 2 }
+ new StageDefinition(2)
};
SetContents(_ => new ManiaPlayfield(stageDefinitions));
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
stageDefinitions = new List
{
- new StageDefinition { Columns = 2 },
- new StageDefinition { Columns = 2 }
+ new StageDefinition(2),
+ new StageDefinition(2)
};
SetContents(_ => new ManiaPlayfield(stageDefinitions));
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
index f3f1b9416f..07aa0b845f 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
- Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
+ Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction)
};
});
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
index 687b3a747d..0744d7e2e7 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
@@ -5,7 +5,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Skinning;
@@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground),
_ => new DefaultStageBackground())
{
Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
index 6cbc172755..979c90c802 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -5,7 +5,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
@@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
index 21ec85bbe6..3abeb8a5f6 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | - |
// | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(1));
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
var generated = new ManiaAutoGenerator(beatmap).Generate();
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * |
// | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(1));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
var generated = new ManiaAutoGenerator(beatmap).Generate();
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | - | - |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 });
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * | * |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 });
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | - | |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 });
@@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * | |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 });
@@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * | |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 });
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
index 2922d18713..83491b6fe9 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
@@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(1);
+
private readonly List columns = new List();
public TestSceneColumn()
@@ -84,12 +88,12 @@ namespace osu.Game.Rulesets.Mania.Tests
private Drawable createColumn(ScrollingDirection direction, ManiaAction action, int index)
{
- var column = new Column(index)
+ var column = new Column(index, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 0.85f,
- AccentColour = Color4.OrangeRed,
+ AccentColour = { Value = Color4.OrangeRed },
Action = { Value = action },
};
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
index 223f8dae44..d273f5cb35 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
@@ -4,11 +4,13 @@
#nullable disable
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
@@ -24,6 +26,9 @@ namespace osu.Game.Rulesets.Mania.Tests
private Column column;
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(1);
+
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -35,11 +40,11 @@ namespace osu.Game.Rulesets.Mania.Tests
RelativeSizeAxes = Axes.Y,
TimeRange = 2000,
Clock = new FramedClock(clock),
- Child = column = new Column(0)
+ Child = column = new Column(0, false)
{
Action = { Value = ManiaAction.Key1 },
Height = 0.85f,
- AccentColour = Color4.Gray
+ AccentColour = { Value = Color4.Gray },
},
};
});
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index a563dc3106..1f139b5b78 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
AddStep("load player", () =>
{
- Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = hitObjects,
BeatmapInfo =
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index cf8947c1ed..6387dac957 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
var specialAction = ManiaAction.Special1;
- var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
+ var stage = new Stage(0, new StageDefinition(2), ref action, ref specialAction);
stages.Add(stage);
return new ScrollingTestContainer(direction)
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
index e84d02775a..9f2e3d2502 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
const double beat_length = 500;
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
+ var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 2951076591..0d7b03d830 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs b/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs
deleted file mode 100644
index 0114987e3c..0000000000
--- a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-namespace osu.Game.Rulesets.Mania.Beatmaps
-{
- public enum ColumnType
- {
- Even,
- Odd,
- Special
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
index 4879ce6748..b5655a4579 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -60,5 +61,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
},
};
}
+
+ public StageDefinition GetStageForColumnIndex(int column)
+ {
+ foreach (var stage in Stages)
+ {
+ if (column < stage.Columns)
+ return stage;
+
+ column -= stage.Columns;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(column), "Provided index exceeds all available stages");
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 90cd7f57b5..632b7cdcc7 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -93,10 +93,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap CreateBeatmap()
{
- beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
+ beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns);
if (Dual)
- beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
+ beatmap.Stages.Add(new StageDefinition(TargetColumns));
return beatmap;
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
index 54e2d4686f..898b558eb3 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
@@ -11,32 +11,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
/// Defines properties for each stage in a .
///
- public struct StageDefinition
+ public class StageDefinition
{
///
/// The number of s which this stage contains.
///
- public int Columns;
+ public readonly int Columns;
+
+ public StageDefinition(int columns)
+ {
+ if (columns < 1)
+ throw new ArgumentException("Column count must be above zero.", nameof(columns));
+
+ Columns = columns;
+ }
///
/// Whether the column index is a special column for this stage.
///
/// The 0-based column index.
/// Whether the column is a special column.
- public readonly bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
-
- ///
- /// Get the type of column given a column index.
- ///
- /// The 0-based column index.
- /// The type of the column.
- public readonly ColumnType GetTypeOfColumn(int column)
- {
- if (IsSpecialColumn(column))
- return ColumnType.Special;
-
- int distanceToEdge = Math.Min(column, (Columns - 1) - column);
- return distanceToEdge % 2 == 0 ? ColumnType.Odd : ColumnType.Even;
- }
+ public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 061dedb07a..6162184c9a 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mania.Edit.Setup;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Mania.Skinning.Argon;
+using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@@ -66,6 +68,15 @@ namespace osu.Game.Rulesets.Mania
{
switch (skin)
{
+ case TrianglesSkin:
+ return new ManiaTrianglesSkinTransformer(skin, beatmap);
+
+ case ArgonSkin:
+ return new ManiaArgonSkinTransformer(skin, beatmap);
+
+ case DefaultLegacySkin:
+ return new ManiaClassicSkinTransformer(skin, beatmap);
+
case LegacySkin:
return new ManiaLegacySkinTransformer(skin, beatmap);
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index 21b362df00..f05edb4677 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -3,29 +3,19 @@
#nullable disable
-using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponent : GameplaySkinComponent
{
- ///
- /// The intended for this component.
- /// May be null if the component is not a direct member of a .
- ///
- public readonly StageDefinition? StageDefinition;
-
///
/// Creates a new .
///
/// The component.
- /// The intended for this component. May be null if the component is not a direct member of a .
- public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null)
+ public ManiaSkinComponent(ManiaSkinComponents component)
: base(component)
{
- StageDefinition = stageDefinition;
}
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index 8ef5bfd94c..6eaede2112 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -5,7 +5,6 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Layout;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osuTK;
@@ -17,22 +16,14 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
- [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableFloat SizeMultiplier { get; } = new BindableFloat
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 3f,
- Default = 1f,
- Value = 1f,
Precision = 0.1f
};
- [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
- public override BindableBool ComboBasedSize { get; } = new BindableBool
- {
- Default = false,
- Value = false
- };
+ public override BindableBool ComboBasedSize { get; } = new BindableBool();
public override float DefaultFlashlightSize => 50;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index 6020348938..a607ed572d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
- protected override void UpdateInitialTransforms()
- {
- }
-
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index d374e935ec..ac646ea427 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -30,20 +30,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public bool UpdateResult() => base.UpdateResult(true);
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- // This hitobject should never expire, so this is just a safe maximum.
- LifetimeEnd = LifetimeStart + 30000;
- }
-
protected override void UpdateHitStateTransforms(ArmedState state)
{
// suppress the base call explicitly.
// the hold note head should never change its visual state on its own due to the "freezing" mechanic
// (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line).
// it will be hidden along with its parenting hold note when required.
+
+ // Set `LifetimeEnd` explicitly to a non-`double.MaxValue` because otherwise this DHO is automatically expired.
+ LifetimeEnd = double.PositiveInfinity;
}
public override bool OnPressed(KeyBindingPressEvent e) => false; // Handled by the hold note
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index a7bdcd047e..3084f71be2 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
- protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
+ public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index bcc10ab7bc..73dc937a00 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -23,10 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable Direction = new Bindable();
- // Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms.
- // Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1.
- protected override double InitialLifetimeOffset => 30000;
-
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
@@ -69,22 +65,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Direction.BindValueChanged(OnDirectionChanged, true);
}
- protected override void OnApply()
- {
- base.OnApply();
-
- if (ParentHitObject != null)
- AccentColour.BindTo(ParentHitObject.AccentColour);
- }
-
- protected override void OnFree()
- {
- base.OnFree();
-
- if (ParentHitObject != null)
- AccentColour.UnbindFrom(ParentHitObject.AccentColour);
- }
-
protected virtual void OnDirectionChanged(ValueChangedEvent e)
{
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs
similarity index 50%
rename from osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
rename to osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs
index 5bd2d3ab48..598a765d3c 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs
@@ -1,8 +1,6 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -12,26 +10,38 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Mania.UI.Components
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
- public class ColumnBackground : CompositeDrawable, IKeyBindingHandler, IHasAccentColour
+ public class ArgonColumnBackground : CompositeDrawable, IKeyBindingHandler
{
- private readonly IBindable action = new Bindable();
-
- private Box background;
- private Box backgroundOverlay;
-
private readonly IBindable direction = new Bindable();
- [BackgroundDependencyLoader]
- private void load(IBindable action, IScrollingInfo scrollingInfo)
- {
- this.action.BindTo(action);
+ private Color4 brightColour;
+ private Color4 dimColour;
+ private Box background = null!;
+ private Box backgroundOverlay = null!;
+
+ [Resolved]
+ private Column column { get; set; } = null!;
+
+ private Bindable accentColour = null!;
+
+ public ArgonColumnBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ Masking = true;
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
InternalChildren = new[]
{
background = new Box
@@ -49,61 +59,42 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
};
- direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(dir =>
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
{
- backgroundOverlay.Anchor = backgroundOverlay.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
- updateColours();
+ background.Colour = colour.NewValue.Darken(3).Opacity(0.8f);
+ brightColour = colour.NewValue.Opacity(0.6f);
+ dimColour = colour.NewValue.Opacity(0);
}, true);
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
}
- protected override void LoadComplete()
+ private void onDirectionChanged(ValueChangedEvent direction)
{
- base.LoadComplete();
- updateColours();
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
+ if (direction.NewValue == ScrollingDirection.Up)
{
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- updateColours();
+ backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.TopLeft;
+ backgroundOverlay.Colour = ColourInfo.GradientVertical(brightColour, dimColour);
+ }
+ else
+ {
+ backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.BottomLeft;
+ backgroundOverlay.Colour = ColourInfo.GradientVertical(dimColour, brightColour);
}
- }
-
- private void updateColours()
- {
- if (!IsLoaded)
- return;
-
- background.Colour = AccentColour.Darken(5);
-
- var brightPoint = AccentColour.Opacity(0.6f);
- var dimPoint = AccentColour.Opacity(0);
-
- backgroundOverlay.Colour = ColourInfo.GradientVertical(
- direction.Value == ScrollingDirection.Up ? brightPoint : dimPoint,
- direction.Value == ScrollingDirection.Up ? dimPoint : brightPoint);
}
public bool OnPressed(KeyBindingPressEvent e)
{
- if (e.Action == action.Value)
+ if (e.Action == column.Action.Value)
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
return false;
}
public void OnReleased(KeyBindingReleaseEvent e)
{
- if (e.Action == action.Value)
+ if (e.Action == column.Action.Value)
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
new file mode 100644
index 0000000000..af179d5580
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonHitExplosion : CompositeDrawable, IHitExplosion
+ {
+ public override bool RemoveWhenNotAlive => true;
+
+ [Resolved]
+ private Column column { get; set; } = null!;
+
+ private readonly IBindable direction = new Bindable();
+
+ private Container largeFaint = null!;
+
+ private Bindable accentColour = null!;
+
+ public ArgonHitExplosion()
+ {
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.X;
+ Height = ArgonNotePiece.NOTE_HEIGHT;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ InternalChildren = new Drawable[]
+ {
+ largeFaint = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS,
+ Blending = BlendingParameters.Additive,
+ Child = new Box
+ {
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1);
+
+ largeFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour.NewValue,
+ Roundness = 40,
+ Radius = 60,
+ };
+ }, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ Anchor = Anchor.TopCentre;
+ Y = ArgonNotePiece.NOTE_HEIGHT / 2;
+ }
+ else
+ {
+ Anchor = Anchor.BottomCentre;
+ Y = -ArgonNotePiece.NOTE_HEIGHT / 2;
+ }
+ }
+
+ public void Animate(JudgementResult result)
+ {
+ this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
new file mode 100644
index 0000000000..9e449623d5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
@@ -0,0 +1,47 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonHitTarget : CompositeDrawable
+ {
+ private readonly IBindable direction = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = ArgonNotePiece.NOTE_HEIGHT;
+
+ Masking = true;
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+
+ InternalChildren = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.3f,
+ Blending = BlendingParameters.Additive,
+ Colour = Color4.White
+ },
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ Anchor = Origin = direction.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
new file mode 100644
index 0000000000..757190c4ae
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Skinning.Default;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ ///
+ /// Represents length-wise portion of a hold note.
+ ///
+ public class ArgonHoldBodyPiece : CompositeDrawable, IHoldNoteBody
+ {
+ protected readonly Bindable AccentColour = new Bindable();
+ protected readonly IBindable IsHitting = new Bindable();
+
+ private Drawable background = null!;
+ private Box foreground = null!;
+
+ public ArgonHoldBodyPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ // Without this, the width of the body will be slightly larger than the head/tail.
+ Masking = true;
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+ Blending = BlendingParameters.Additive;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(DrawableHitObject? drawableObject)
+ {
+ InternalChildren = new[]
+ {
+ background = new Box { RelativeSizeAxes = Axes.Both },
+ foreground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0,
+ },
+ };
+
+ if (drawableObject != null)
+ {
+ var holdNote = (DrawableHoldNote)drawableObject;
+
+ AccentColour.BindTo(holdNote.AccentColour);
+ IsHitting.BindTo(holdNote.IsHitting);
+ }
+
+ AccentColour.BindValueChanged(colour =>
+ {
+ background.Colour = colour.NewValue.Darken(1.2f);
+ foreground.Colour = colour.NewValue.Opacity(0.2f);
+ }, true);
+
+ IsHitting.BindValueChanged(hitting =>
+ {
+ const float animation_length = 50;
+
+ foreground.ClearTransforms();
+
+ if (hitting.NewValue)
+ {
+ // wait for the next sync point
+ double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
+
+ using (foreground.BeginDelayedSequence(synchronisedOffset))
+ {
+ foreground.FadeTo(1, animation_length).Then()
+ .FadeTo(0.5f, animation_length)
+ .Loop();
+ }
+ }
+ else
+ {
+ foreground.FadeOut(animation_length);
+ }
+ });
+ }
+
+ public void Recycle()
+ {
+ foreground.ClearTransforms();
+ foreground.Alpha = 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
new file mode 100644
index 0000000000..e1068c6cd8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ internal class ArgonHoldNoteTailPiece : CompositeDrawable
+ {
+ private readonly IBindable direction = new Bindable();
+ private readonly IBindable accentColour = new Bindable();
+
+ private readonly Box colouredBox;
+ private readonly Box shadow;
+
+ public ArgonHoldNoteTailPiece()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = ArgonNotePiece.NOTE_HEIGHT;
+
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ shadow = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.82f,
+ Masking = true,
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS,
+ Children = new Drawable[]
+ {
+ colouredBox = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ }
+ },
+ new Circle
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = ArgonNotePiece.CORNER_RADIUS * 2,
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ if (drawableObject != null)
+ {
+ accentColour.BindTo(drawableObject.AccentColour);
+ accentColour.BindValueChanged(onAccentChanged, true);
+ }
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopCentre
+ : Anchor.BottomCentre;
+ }
+
+ private void onAccentChanged(ValueChangedEvent accent)
+ {
+ colouredBox.Colour = ColourInfo.GradientVertical(
+ accent.NewValue,
+ accent.NewValue.Darken(0.1f)
+ );
+
+ shadow.Colour = accent.NewValue.Darken(0.5f);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
new file mode 100644
index 0000000000..e7dfec256d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
@@ -0,0 +1,193 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; } = null!;
+
+ private RingExplosion? ringExplosion;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ArgonJudgementPiece(HitResult result)
+ {
+ Result = result;
+ Origin = Anchor.Centre;
+ Y = 160;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
+ },
+ };
+
+ if (Result.IsHit())
+ {
+ AddInternal(ringExplosion = new RingExplosion(Result)
+ {
+ Colour = colours.ForHitResult(Result),
+ });
+ }
+ }
+
+ ///
+ /// Plays the default animation for this judgement piece.
+ ///
+ ///
+ /// The base implementation only handles fade (for all result types) and misses.
+ /// Individual rulesets are recommended to implement their appropriate hit animations.
+ ///
+ public virtual void PlayAnimation()
+ {
+ switch (Result)
+ {
+ default:
+ JudgementText
+ .ScaleTo(Vector2.One)
+ .ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
+ break;
+
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ this.MoveTo(Vector2.Zero);
+ this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, 800, Easing.InQuint);
+ break;
+ }
+
+ this.FadeOutFromOne(800);
+
+ ringExplosion?.PlayAnimation();
+ }
+
+ public Drawable? GetAboveHitObjectsProxiedContent() => null;
+
+ private class RingExplosion : CompositeDrawable
+ {
+ private readonly float travel = 52;
+
+ public RingExplosion(HitResult result)
+ {
+ const float thickness = 4;
+
+ const float small_size = 9;
+ const float large_size = 14;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Blending = BlendingParameters.Additive;
+
+ int countSmall = 0;
+ int countLarge = 0;
+
+ switch (result)
+ {
+ case HitResult.Meh:
+ countSmall = 3;
+ travel *= 0.3f;
+ break;
+
+ case HitResult.Ok:
+ case HitResult.Good:
+ countSmall = 4;
+ travel *= 0.6f;
+ break;
+
+ case HitResult.Great:
+ case HitResult.Perfect:
+ countSmall = 4;
+ countLarge = 4;
+ break;
+ }
+
+ for (int i = 0; i < countSmall; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
+
+ for (int i = 0; i < countLarge; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
+ }
+
+ public void PlayAnimation()
+ {
+ foreach (var c in InternalChildren)
+ {
+ const float start_position_ratio = 0.3f;
+
+ float direction = RNG.NextSingle(0, 360);
+ float distance = RNG.NextSingle(travel / 2, travel);
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance * start_position_ratio,
+ MathF.Sin(direction) * distance * start_position_ratio
+ ));
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance,
+ MathF.Sin(direction) * distance
+ ), 600, Easing.OutQuint);
+ }
+
+ this.FadeOutFromOne(1000, Easing.OutQuint);
+ }
+
+ public class RingPiece : CircularContainer
+ {
+ public RingPiece(float thickness = 9)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Masking = true;
+ BorderThickness = thickness;
+ BorderColour = Color4.White;
+
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs
new file mode 100644
index 0000000000..7670c9bdf2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs
@@ -0,0 +1,272 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonKeyArea : CompositeDrawable, IKeyBindingHandler
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Container directionContainer = null!;
+ private Drawable background = null!;
+
+ private Circle hitTargetLine = null!;
+
+ private Container bottomIcon = null!;
+ private CircularContainer topIcon = null!;
+
+ private Bindable accentColour = null!;
+
+ [Resolved]
+ private Column column { get; set; } = null!;
+
+ public ArgonKeyArea()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ const float icon_circle_size = 8;
+ const float icon_spacing = 7;
+ const float icon_vertical_offset = -30;
+
+ InternalChild = directionContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ // Ensure the area is tall enough to put the target line in the correct location.
+ // This is to also allow the main background component to overlap the target line
+ // and avoid an inner corner radius being shown below the target line.
+ Height = Stage.HIT_TARGET_POSITION + ArgonNotePiece.CORNER_RADIUS * 2,
+ Children = new[]
+ {
+ new Container
+ {
+ Masking = true,
+ RelativeSizeAxes = Axes.Both,
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS,
+ Child = background = new Box
+ {
+ Name = "Key gradient",
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ hitTargetLine = new Circle
+ {
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Colour = OsuColour.Gray(196 / 255f),
+ Height = ArgonNotePiece.CORNER_RADIUS * 2,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ new Container
+ {
+ Name = "Icons",
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Children = new Drawable[]
+ {
+ bottomIcon = new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Y = icon_vertical_offset,
+ Children = new[]
+ {
+ new Circle
+ {
+ Size = new Vector2(icon_circle_size),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ new Circle
+ {
+ X = -icon_spacing,
+ Y = icon_spacing * 1.2f,
+ Size = new Vector2(icon_circle_size),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ new Circle
+ {
+ X = icon_spacing,
+ Y = icon_spacing * 1.2f,
+ Size = new Vector2(icon_circle_size),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ }
+ },
+ topIcon = new CircularContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Y = -icon_vertical_offset,
+ Size = new Vector2(22, 14),
+ Masking = true,
+ BorderThickness = 4,
+ BorderColour = Color4.White,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ },
+ }
+ }
+ },
+ }
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ background.Colour = colour.NewValue.Darken(0.2f);
+ bottomIcon.Colour = colour.NewValue;
+ },
+ true);
+
+ // Yes, proxy everything.
+ column.TopLevelContainer.Add(CreateProxy());
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ switch (direction.NewValue)
+ {
+ case ScrollingDirection.Up:
+ directionContainer.Scale = new Vector2(1, -1);
+ directionContainer.Anchor = Anchor.TopLeft;
+ directionContainer.Origin = Anchor.BottomLeft;
+ break;
+
+ case ScrollingDirection.Down:
+ directionContainer.Scale = new Vector2(1, 1);
+ directionContainer.Anchor = Anchor.BottomLeft;
+ directionContainer.Origin = Anchor.BottomLeft;
+ break;
+ }
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Action != column.Action.Value) return false;
+
+ const double lighting_fade_in_duration = 70;
+ Color4 lightingColour = getLightingColour();
+
+ background
+ .FlashColour(accentColour.Value.Lighten(0.8f), 200, Easing.OutQuint)
+ .FadeTo(1, lighting_fade_in_duration, Easing.OutQuint)
+ .Then()
+ .FadeTo(0.8f, 500);
+
+ hitTargetLine.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint);
+ hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour.Opacity(0.4f),
+ Radius = 20,
+ }, lighting_fade_in_duration, Easing.OutQuint);
+
+ topIcon.ScaleTo(0.9f, lighting_fade_in_duration, Easing.OutQuint);
+ topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour.Opacity(0.1f),
+ Radius = 20,
+ }, lighting_fade_in_duration, Easing.OutQuint);
+
+ bottomIcon.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint);
+
+ foreach (var circle in bottomIcon)
+ {
+ circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour.Opacity(0.2f),
+ Radius = 60,
+ }, lighting_fade_in_duration, Easing.OutQuint);
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ if (e.Action != column.Action.Value) return;
+
+ const double lighting_fade_out_duration = 800;
+
+ Color4 lightingColour = getLightingColour().Opacity(0);
+
+ // background fades out faster than lighting elements to give better definition to the player.
+ background.FadeTo(0.3f, 50, Easing.OutQuint)
+ .Then()
+ .FadeOut(lighting_fade_out_duration, Easing.OutQuint);
+
+ topIcon.ScaleTo(1f, 200, Easing.OutQuint);
+ topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour,
+ Radius = 20,
+ }, lighting_fade_out_duration, Easing.OutQuint);
+
+ hitTargetLine.FadeColour(OsuColour.Gray(196 / 255f), lighting_fade_out_duration, Easing.OutQuint);
+ hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour,
+ Radius = 25,
+ }, lighting_fade_out_duration, Easing.OutQuint);
+
+ bottomIcon.FadeColour(accentColour.Value, lighting_fade_out_duration, Easing.OutQuint);
+
+ foreach (var circle in bottomIcon)
+ {
+ circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour,
+ Radius = 30,
+ }, lighting_fade_out_duration, Easing.OutQuint);
+ }
+ }
+
+ private Color4 getLightingColour() => Interpolation.ValueAt(0.2f, accentColour.Value, Color4.White, 0, 1);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
new file mode 100644
index 0000000000..454a6b012b
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
@@ -0,0 +1,110 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ internal class ArgonNotePiece : CompositeDrawable
+ {
+ public const float NOTE_HEIGHT = 42;
+
+ public const float CORNER_RADIUS = 3.4f;
+
+ private readonly IBindable direction = new Bindable();
+ private readonly IBindable accentColour = new Bindable();
+
+ private readonly Box colouredBox;
+ private readonly Box shadow;
+
+ public ArgonNotePiece()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = NOTE_HEIGHT;
+
+ CornerRadius = CORNER_RADIUS;
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ shadow = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.82f,
+ Masking = true,
+ CornerRadius = CORNER_RADIUS,
+ Children = new Drawable[]
+ {
+ colouredBox = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ }
+ },
+ new Circle
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.X,
+ Height = CORNER_RADIUS * 2,
+ },
+ new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 4,
+ Icon = FontAwesome.Solid.AngleDown,
+ Size = new Vector2(20),
+ Scale = new Vector2(1, 0.7f)
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ if (drawableObject != null)
+ {
+ accentColour.BindTo(drawableObject.AccentColour);
+ accentColour.BindValueChanged(onAccentChanged, true);
+ }
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopCentre
+ : Anchor.BottomCentre;
+ }
+
+ private void onAccentChanged(ValueChangedEvent accent)
+ {
+ colouredBox.Colour = ColourInfo.GradientVertical(
+ accent.NewValue.Lighten(0.1f),
+ accent.NewValue
+ );
+
+ shadow.Colour = accent.NewValue.Darken(0.5f);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs
new file mode 100644
index 0000000000..1881695b14
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . 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.Mania.Skinning.Argon
+{
+ public class ArgonStageBackground : CompositeDrawable
+ {
+ public ArgonStageBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
new file mode 100644
index 0000000000..27926c11eb
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -0,0 +1,139 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ManiaArgonSkinTransformer : SkinTransformer
+ {
+ private readonly ManiaBeatmap beatmap;
+
+ public ManiaArgonSkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin)
+ {
+ this.beatmap = (ManiaBeatmap)beatmap;
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponent component)
+ {
+ switch (component)
+ {
+ case GameplaySkinComponent resultComponent:
+ return new ArgonJudgementPiece(resultComponent.Component);
+
+ case ManiaSkinComponent maniaComponent:
+ // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
+ switch (maniaComponent.Component)
+ {
+ case ManiaSkinComponents.StageBackground:
+ return new ArgonStageBackground();
+
+ case ManiaSkinComponents.ColumnBackground:
+ return new ArgonColumnBackground();
+
+ case ManiaSkinComponents.HoldNoteBody:
+ return new ArgonHoldBodyPiece();
+
+ case ManiaSkinComponents.HoldNoteTail:
+ return new ArgonHoldNoteTailPiece();
+
+ case ManiaSkinComponents.HoldNoteHead:
+ case ManiaSkinComponents.Note:
+ return new ArgonNotePiece();
+
+ case ManiaSkinComponents.HitTarget:
+ return new ArgonHitTarget();
+
+ case ManiaSkinComponents.KeyArea:
+ return new ArgonKeyArea();
+
+ case ManiaSkinComponents.HitExplosion:
+ return new ArgonHitExplosion();
+ }
+
+ break;
+ }
+
+ return base.GetDrawableComponent(component);
+ }
+
+ public override IBindable? GetConfig(TLookup lookup)
+ {
+ if (lookup is ManiaSkinConfigurationLookup maniaLookup)
+ {
+ int column = maniaLookup.ColumnIndex ?? 0;
+ var stage = beatmap.GetStageForColumnIndex(column);
+
+ switch (maniaLookup.Lookup)
+ {
+ case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
+ return SkinUtils.As(new Bindable(2));
+
+ case LegacyManiaSkinConfigurationLookups.StagePaddingBottom:
+ case LegacyManiaSkinConfigurationLookups.StagePaddingTop:
+ return SkinUtils.As(new Bindable(30));
+
+ case LegacyManiaSkinConfigurationLookups.ColumnWidth:
+ return SkinUtils.As(new Bindable(
+ stage.IsSpecialColumn(column) ? 120 : 60
+ ));
+
+ case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
+
+ Color4 colour;
+
+ if (stage.IsSpecialColumn(column))
+ colour = new Color4(159, 101, 255, 255);
+ else
+ {
+ switch (column % 8)
+ {
+ default:
+ colour = new Color4(240, 216, 0, 255);
+ break;
+
+ case 1:
+ colour = new Color4(240, 101, 0, 255);
+ break;
+
+ case 2:
+ colour = new Color4(240, 0, 130, 255);
+ break;
+
+ case 3:
+ colour = new Color4(192, 0, 240, 255);
+ break;
+
+ case 4:
+ colour = new Color4(178, 0, 240, 255);
+ break;
+
+ case 5:
+ colour = new Color4(0, 96, 240, 255);
+ break;
+
+ case 6:
+ colour = new Color4(0, 226, 240, 255);
+ break;
+
+ case 7:
+ colour = new Color4(0, 240, 96, 255);
+ break;
+ }
+ }
+
+ return SkinUtils.As(new Bindable(colour));
+ }
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs
new file mode 100644
index 0000000000..eb51179cea
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Default
+{
+ public class ManiaTrianglesSkinTransformer : SkinTransformer
+ {
+ private readonly ManiaBeatmap beatmap;
+
+ public ManiaTrianglesSkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin)
+ {
+ this.beatmap = (ManiaBeatmap)beatmap;
+ }
+
+ private readonly Color4 colourEven = new Color4(6, 84, 0, 255);
+ private readonly Color4 colourOdd = new Color4(94, 0, 57, 255);
+ private readonly Color4 colourSpecial = new Color4(0, 48, 63, 255);
+
+ public override IBindable? GetConfig(TLookup lookup)
+ {
+ if (lookup is ManiaSkinConfigurationLookup maniaLookup)
+ {
+ switch (maniaLookup.Lookup)
+ {
+ case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
+ int column = maniaLookup.ColumnIndex ?? 0;
+
+ var stage = beatmap.GetStageForColumnIndex(column);
+
+ if (stage.IsSpecialColumn(column))
+ return SkinUtils.As(new Bindable(colourSpecial));
+
+ int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column);
+ return SkinUtils.As(new Bindable(distanceToEdge % 2 == 0 ? colourOdd : colourEven));
+ }
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
index ab953ccfb9..e227c80845 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
@@ -20,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[Resolved]
protected Column Column { get; private set; }
+ [Resolved]
+ private StageDefinition stage { get; set; }
+
///
/// The column type identifier to use for texture lookups, in the case of no user-provided configuration.
///
@@ -28,19 +32,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader]
private void load()
{
- switch (Column.ColumnType)
+ if (Column.IsSpecial)
+ FallbackColumnIndex = "S";
+ else
{
- case ColumnType.Special:
- FallbackColumnIndex = "S";
- break;
-
- case ColumnType.Odd:
- FallbackColumnIndex = "1";
- break;
-
- case ColumnType.Even:
- FallbackColumnIndex = "2";
- break;
+ int distanceToEdge = Math.Min(Column.Index, (stage.Columns - 1) - Column.Index);
+ FallbackColumnIndex = distanceToEdge % 2 == 0 ? "1" : "2";
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
index 740ccbfe27..d039551cd7 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
@@ -18,20 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class LegacyStageBackground : CompositeDrawable
{
- private readonly StageDefinition stageDefinition;
-
private Drawable leftSprite;
private Drawable rightSprite;
private ColumnFlow columnBackgrounds;
- public LegacyStageBackground(StageDefinition stageDefinition)
+ public LegacyStageBackground()
{
- this.stageDefinition = stageDefinition;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
- private void load(ISkinSource skin)
+ private void load(ISkinSource skin, StageDefinition stageDefinition)
{
string leftImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
?? "mania-stage-left";
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs
new file mode 100644
index 0000000000..e57927897c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Legacy
+{
+ public class ManiaClassicSkinTransformer : ManiaLegacySkinTransformer
+ {
+ public ManiaClassicSkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin, beatmap)
+ {
+ }
+
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ if (lookup is ManiaSkinConfigurationLookup maniaLookup)
+ {
+ var baseLookup = base.GetConfig(lookup);
+
+ if (baseLookup != null)
+ return baseLookup;
+
+ // default provisioning.
+ switch (maniaLookup.Lookup)
+ {
+ case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
+ return SkinUtils.As(new Bindable(Color4.Black));
+ }
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index dd5baa8150..1d39721a2b 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -20,8 +19,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class ManiaLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ManiaBeatmap beatmap;
-
///
/// Mapping of to their corresponding
/// value.
@@ -60,6 +57,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
///
private readonly Lazy hasKeyTexture;
+ private readonly ManiaBeatmap beatmap;
+
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
: base(skin)
{
@@ -113,8 +112,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new LegacyHitExplosion();
case ManiaSkinComponents.StageBackground:
- Debug.Assert(maniaComponent.StageDefinition != null);
- return new LegacyStageBackground(maniaComponent.StageDefinition.Value);
+ return new LegacyStageBackground();
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
@@ -151,7 +149,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
public override IBindable GetConfig(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
- return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
+ {
+ return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.ColumnIndex));
+ }
return base.GetConfig(lookup);
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
index 4d0c321116..e22bf63049 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
@@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
///
/// The skin from which configuration is retrieved.
/// The value to retrieve.
- /// If not null, denotes the index of the column to which the entry applies.
- public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null)
+ /// If not null, denotes the index of the column to which the entry applies.
+ public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null)
=> skin.GetConfig(
- new ManiaSkinConfigurationLookup(lookup, index));
+ new ManiaSkinConfigurationLookup(lookup, columnIndex));
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
index e9005a3da0..59188f02f9 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
@@ -16,20 +16,21 @@ namespace osu.Game.Rulesets.Mania.Skinning
public readonly LegacyManiaSkinConfigurationLookups Lookup;
///
- /// The intended index for the configuration.
+ /// The column which is being looked up.
/// May be null if the configuration does not apply to a .
+ /// Note that this is the absolute index across all stages.
///
- public readonly int? TargetColumn;
+ public readonly int? ColumnIndex;
///
/// Creates a new .
///
/// The lookup value.
- /// The intended index for the configuration. May be null if the configuration does not apply to a .
- public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null)
+ /// The intended index for the configuration. May be null if the configuration does not apply to a .
+ public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null)
{
Lookup = lookup;
- TargetColumn = targetColumn;
+ ColumnIndex = columnIndex;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index deb1b155b5..3d46bdaa7b 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -3,30 +3,30 @@
#nullable disable
-using osuTK.Graphics;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Platform;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
-using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.UI;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
[Cached]
- public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour
+ public class Column : ScrollingPlayfield, IKeyBindingHandler
{
public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70;
@@ -39,23 +39,46 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
public readonly ColumnHitObjectArea HitObjectArea;
- internal readonly Container TopLevelContainer;
- private readonly DrawablePool hitExplosionPool;
+ internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
+ private DrawablePool hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
- private readonly GameplaySampleTriggerSource sampleTriggerSource;
+ private GameplaySampleTriggerSource sampleTriggerSource;
- public Column(int index)
+ ///
+ /// Whether this is a special (ie. scratch) column.
+ ///
+ public readonly bool IsSpecial;
+
+ public readonly Bindable AccentColour = new Bindable(Color4.Black);
+
+ public Column(int index, bool isSpecial)
{
Index = index;
+ IsSpecial = isSpecial;
RelativeSizeAxes = Axes.Y;
Width = COLUMN_WIDTH;
+ hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+ HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both };
+ }
+
+ [Resolved]
+ private ISkinSource skin { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host)
+ {
+ SkinnableDrawable keyArea;
+
+ skin.SourceChanged += onSourceChanged;
+ onSourceChanged();
+
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
};
InternalChildren = new[]
@@ -64,17 +87,18 @@ namespace osu.Game.Rulesets.Mania.UI
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
- HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both },
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
+ HitObjectArea,
+ keyArea = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
},
background,
- TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both },
+ TopLevelContainer,
new ColumnTouchInputArea(this)
};
- hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+ applyGameWideClock(background);
+ applyGameWideClock(keyArea);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
@@ -83,20 +107,38 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool(10, 50);
RegisterPool(10, 50);
RegisterPool(50, 250);
+
+ // Some elements don't handle rewind correctly and fixing them is non-trivial.
+ // In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide
+ // clock so they don't need to worry about rewind.
+ // This only works because they handle OnPressed/OnReleased which results in a correct state while rewinding.
+ //
+ // This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind.
+ void applyGameWideClock(Drawable drawable)
+ {
+ drawable.Clock = host.UpdateThread.Clock;
+ drawable.ProcessCustomClock = false;
+ }
+ }
+
+ private void onSourceChanged()
+ {
+ AccentColour.Value = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black;
}
protected override void LoadComplete()
{
base.LoadComplete();
-
NewResult += OnNewResult;
}
- public ColumnType ColumnType { get; set; }
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
- public bool IsSpecial => ColumnType == ColumnType.Special;
-
- public Color4 AccentColour { get; set; }
+ if (skin != null)
+ skin.SourceChanged -= onSourceChanged;
+ }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -111,7 +153,7 @@ namespace osu.Game.Rulesets.Mania.UI
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject;
- maniaObject.AccentColour.Value = AccentColour;
+ maniaObject.AccentColour.BindTo(AccentColour);
maniaObject.CheckHittable = hitPolicy.IsHittable;
}
diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
index 871ec9f1a3..9b3f6d7033 100644
--- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X;
+ Masking = true;
+
InternalChild = columns = new FillFlowContainer
{
RelativeSizeAxes = Axes.Y,
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
index 39d17db6be..3680e7ea0a 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
[Resolved]
private Column column { get; set; }
+ private Bindable accentColour;
+
public DefaultColumnBackground()
{
RelativeSizeAxes = Axes.Both;
@@ -55,9 +57,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
};
- background.Colour = column.AccentColour.Darken(5);
- brightColour = column.AccentColour.Opacity(0.6f);
- dimColour = column.AccentColour.Opacity(0);
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ background.Colour = colour.NewValue.Darken(5);
+ brightColour = colour.NewValue.Opacity(0.6f);
+ dimColour = colour.NewValue.Opacity(0);
+ }, true);
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
index 53fa86125f..97aa897782 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private Container hitTargetLine;
private Drawable hitTargetBar;
+ private Bindable accentColour;
+
[Resolved]
private Column column { get; set; }
@@ -54,12 +56,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components
},
};
- hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
{
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = column.AccentColour.Opacity(0.5f),
- };
+ hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = colour.NewValue.Opacity(0.5f),
+ };
+ }, true);
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
index 5a0fab2ff4..600c9feb73 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private Container keyIcon;
private Drawable gradient;
+ private Bindable accentColour;
+
[Resolved]
private Column column { get; set; }
@@ -75,15 +77,19 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
};
- keyIcon.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = column.AccentColour.Opacity(0.5f),
- };
-
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ keyIcon.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = colour.NewValue.Opacity(0.5f),
+ };
+ }, true);
}
private void onDirectionChanged(ValueChangedEvent direction)
diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index e83cd10d2d..59716ee3e2 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -32,6 +32,10 @@ namespace osu.Game.Rulesets.Mania.UI
private CircularContainer largeFaint;
private CircularContainer mainGlow1;
+ private CircularContainer mainGlow2;
+ private CircularContainer mainGlow3;
+
+ private Bindable accentColour;
public DefaultHitExplosion()
{
@@ -48,8 +52,6 @@ namespace osu.Game.Rulesets.Mania.UI
const float roundness = 80;
const float initial_height = 10;
- var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1);
-
InternalChildren = new Drawable[]
{
largeFaint = new CircularContainer
@@ -61,13 +63,6 @@ namespace osu.Game.Rulesets.Mania.UI
// we want our size to be very small so the glow dominates it.
Size = new Vector2(default_large_faint_size),
Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f),
- Roundness = 160,
- Radius = 200,
- },
},
mainGlow1 = new CircularContainer
{
@@ -76,15 +71,8 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1),
- Roundness = 20,
- Radius = 50,
- },
},
- new CircularContainer
+ mainGlow2 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -93,15 +81,8 @@ namespace osu.Game.Rulesets.Mania.UI
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
Rotation = RNG.NextSingle(-angle_variance, angle_variance),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
},
- new CircularContainer
+ mainGlow3 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -110,18 +91,44 @@ namespace osu.Game.Rulesets.Mania.UI
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
Rotation = RNG.NextSingle(-angle_variance, angle_variance),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
}
};
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ largeFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.1f, colour.NewValue, Color4.White, 0, 1).Opacity(0.3f),
+ Roundness = 160,
+ Radius = 200,
+ };
+ mainGlow1.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.6f, colour.NewValue, Color4.White, 0, 1),
+ Roundness = 20,
+ Radius = 50,
+ };
+ mainGlow2.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.4f, colour.NewValue, Color4.White, 0, 1),
+ Roundness = roundness,
+ Radius = 40,
+ };
+ mainGlow3.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.4f, colour.NewValue, Color4.White, 0, 1),
+ Roundness = roundness,
+ Radius = 40,
+ };
+ }, true);
}
private void onDirectionChanged(ValueChangedEvent direction)
diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
index 28509d1f4e..a7b94f9f22 100644
--- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Mania.UI
{
base.PrepareForUse();
+ LifetimeStart = Time.Current;
+
(skinnableExplosion?.Drawable as IHitExplosion)?.Animate(Result);
this.Delay(DURATION).Then().Expire();
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index c578bbb703..1273cb3d32 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -19,7 +21,6 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Mania.UI
///
public class Stage : ScrollingPlayfield
{
+ [Cached]
+ public readonly StageDefinition Definition;
+
public const float COLUMN_SPACING = 1;
public const float HIT_TARGET_POSITION = 110;
@@ -40,13 +44,6 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Drawable barLineContainer;
- private readonly Dictionary columnColours = new Dictionary
- {
- { ColumnType.Even, new Color4(6, 84, 0, 255) },
- { ColumnType.Odd, new Color4(94, 0, 57, 255) },
- { ColumnType.Special, new Color4(0, 48, 63, 255) }
- };
-
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos));
private readonly int firstColumnIndex;
@@ -54,6 +51,7 @@ namespace osu.Game.Rulesets.Mania.UI
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
+ Definition = definition;
Name = "Stage";
@@ -75,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground())
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
{
RelativeSizeAxes = Axes.Both
},
@@ -100,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y,
}
},
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null)
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
{
RelativeSizeAxes = Axes.Both
},
@@ -118,15 +116,13 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < definition.Columns; i++)
{
- var columnType = definition.GetTypeOfColumn(i);
+ bool isSpecial = definition.IsSpecialColumn(i);
- var column = new Column(firstColumnIndex + i)
+ var column = new Column(firstColumnIndex + i, isSpecial)
{
RelativeSizeAxes = Axes.Both,
Width = 1,
- ColumnType = columnType,
- AccentColour = columnColours[columnType],
- Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ }
+ Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ }
};
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
@@ -135,6 +131,37 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
+ private ISkinSource currentSkin;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ currentSkin = skin;
+
+ skin.SourceChanged += onSkinChanged;
+ onSkinChanged();
+ }
+
+ private void onSkinChanged()
+ {
+ float paddingTop = currentSkin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.StagePaddingTop))?.Value ?? 0;
+ float paddingBottom = currentSkin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.StagePaddingBottom))?.Value ?? 0;
+
+ Padding = new MarginPadding
+ {
+ Top = paddingTop,
+ Bottom = paddingBottom,
+ };
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (currentSkin != null)
+ currentSkin.SourceChanged -= onSkinChanged;
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
index 5e46498aca..521c10c10c 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
@@ -125,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
/// Ensures alternation is reset before the first hitobject after a break.
///
[Test]
- public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
+ public void TestInputSingularWithBreak([Values] bool pressBeforeSecondObject) => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
@@ -155,21 +156,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
},
}
},
- ReplayFrames = new List
+ ReplayFrames = new ReplayFrame[]
{
// first press to start alternate lock.
- new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
- new OsuReplayFrame(501, new Vector2(100)),
- // press same key after break but before hit object.
- new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
- new OsuReplayFrame(2251, new Vector2(300, 100)),
+ new OsuReplayFrame(450, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(451, new Vector2(100)),
// press same key at second hitobject and ensure it has been hit.
- new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
- new OsuReplayFrame(2501, new Vector2(500, 100)),
+ new OsuReplayFrame(2450, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2451, new Vector2(500, 100)),
// press same key at third hitobject and ensure it has been missed.
- new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
- new OsuReplayFrame(3001, new Vector2(500, 100)),
- }
+ new OsuReplayFrame(2950, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2951, new Vector2(500, 100)),
+ }.Concat(!pressBeforeSecondObject
+ ? Enumerable.Empty()
+ : new ReplayFrame[]
+ {
+ // press same key after break but before hit object.
+ new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2251, new Vector2(300, 100)),
+ }
+ ).ToList()
});
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
index 9d06ff5801..88b6b9dd56 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
@@ -88,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
if (!objects.Any())
return false;
- return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Children.OfType().Single().Scale.X, target));
+ return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Scale.X, target));
}
private bool checkSomeHit()
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
new file mode 100644
index 0000000000..c24ba6d530
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
@@ -0,0 +1,102 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModRandom : OsuModTestScene
+ {
+ [TestCase(1)]
+ [TestCase(7)]
+ [TestCase(10)]
+ public void TestDefaultBeatmap(float angleSharpness) => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRandom
+ {
+ AngleSharpness = { Value = angleSharpness }
+ },
+ Autoplay = true,
+ PassCondition = () => true
+ });
+
+ [TestCase(1)]
+ [TestCase(7)]
+ [TestCase(10)]
+ public void TestJumpBeatmap(float angleSharpness) => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRandom
+ {
+ AngleSharpness = { Value = angleSharpness }
+ },
+ Beatmap = jumpBeatmap,
+ Autoplay = true,
+ PassCondition = () => true
+ });
+
+ [TestCase(1)]
+ [TestCase(7)]
+ [TestCase(10)]
+ public void TestStreamBeatmap(float angleSharpness) => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRandom
+ {
+ AngleSharpness = { Value = angleSharpness }
+ },
+ Beatmap = streamBeatmap,
+ Autoplay = true,
+ PassCondition = () => true
+ });
+
+ private OsuBeatmap jumpBeatmap =>
+ createHitCircleBeatmap(new[] { 100, 200, 300, 400 }, 8, 300, 2 * 300);
+
+ private OsuBeatmap streamBeatmap =>
+ createHitCircleBeatmap(new[] { 10, 20, 30, 40, 50, 60, 70, 80 }, 16, 150, 4 * 150);
+
+ private OsuBeatmap createHitCircleBeatmap(IEnumerable spacings, int objectsPerSpacing, int interval, int beatLength)
+ {
+ var controlPointInfo = new ControlPointInfo();
+ controlPointInfo.Add(0, new TimingControlPoint
+ {
+ Time = 0,
+ BeatLength = beatLength
+ });
+
+ var beatmap = new OsuBeatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ StackLeniency = 0,
+ Difficulty = new BeatmapDifficulty
+ {
+ ApproachRate = 8.5f
+ }
+ },
+ ControlPointInfo = controlPointInfo
+ };
+
+ foreach (int spacing in spacings)
+ {
+ for (int i = 0; i < objectsPerSpacing; i++)
+ {
+ beatmap.HitObjects.Add(new HitCircle
+ {
+ StartTime = interval * beatmap.HitObjects.Count,
+ Position = beatmap.HitObjects.Count % 2 == 0 ? Vector2.Zero : new Vector2(spacing, 0),
+ NewCombo = i == 0
+ });
+ }
+ }
+
+ return beatmap;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png
new file mode 100644
index 0000000000..b1380a47a4
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
new file mode 100644
index 0000000000..5f7beae4e9
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
index b5d1c4854c..7f0ecaca2b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("setup default legacy skin", () =>
{
- skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
+ skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
});
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index cc69054e23..be224b88ce 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -58,10 +58,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
- var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
-
var playfield = new TestOsuPlayfield();
- playfield.Add(drawable);
+
+ for (double t = timeOffset; t < timeOffset + 60000; t += 2000)
+ playfield.Add(createSingle(circleSize, auto, t, positionOffset));
+
return playfield;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
index 0cf2ec6b7e..57734236da 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- Child = new SkinProvidingContainer(new DefaultSkin(null))
+ Child = new SkinProvidingContainer(new TrianglesSkin(null))
{
RelativeSizeAxes = Axes.Both,
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
new file mode 100644
index 0000000000..1cb64b71fc
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
@@ -0,0 +1,136 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Framework.Input.States;
+using osu.Framework.Logging;
+using osu.Framework.Testing.Input;
+using osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneSmoke : OsuSkinnableTestScene
+ {
+ [Test]
+ public void TestSmoking()
+ {
+ addStep("Create short smoke", 2_000);
+ addStep("Create medium smoke", 5_000);
+ addStep("Create long smoke", 10_000);
+ }
+
+ private void addStep(string stepName, double duration)
+ {
+ var smokeContainers = new List();
+
+ AddStep(stepName, () =>
+ {
+ smokeContainers.Clear();
+ SetContents(_ =>
+ {
+ smokeContainers.Add(new TestSmokeContainer
+ {
+ Duration = duration,
+ RelativeSizeAxes = Axes.Both
+ });
+
+ return new SmokingInputManager
+ {
+ Duration = duration,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.95f),
+ Child = smokeContainers[^1],
+ };
+ });
+ });
+
+ AddUntilStep("Until skinnable expires", () =>
+ {
+ if (smokeContainers.Count == 0)
+ return false;
+
+ Logger.Log("How many: " + smokeContainers.Count);
+
+ foreach (var smokeContainer in smokeContainers)
+ {
+ if (smokeContainer.Children.Count != 0)
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ private class SmokingInputManager : ManualInputManager
+ {
+ public double Duration { get; init; }
+
+ private double? startTime;
+
+ public SmokingInputManager()
+ {
+ UseParentInput = false;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ MoveMouseTo(ToScreenSpace(DrawSize / 2));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ const float spin_angle = 4 * MathF.PI;
+
+ startTime ??= Time.Current;
+
+ float fraction = (float)((Time.Current - startTime) / Duration);
+
+ float angle = fraction * spin_angle;
+ float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2;
+
+ Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2;
+ MoveMouseTo(ToScreenSpace(pos));
+ }
+ }
+
+ private class TestSmokeContainer : SmokeContainer
+ {
+ public double Duration { get; init; }
+
+ private bool isPressing;
+ private bool isFinished;
+
+ private double? startTime;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ startTime ??= Time.Current + 0.1;
+
+ if (!isPressing && !isFinished && Time.Current > startTime)
+ {
+ OnPressed(new KeyBindingPressEvent(new InputState(), OsuAction.Smoke));
+ isPressing = true;
+ isFinished = false;
+ }
+
+ if (isPressing && Time.Current > startTime + Duration)
+ {
+ OnReleased(new KeyBindingReleaseEvent(new InputState(), OsuAction.Smoke));
+ isPressing = false;
+ isFinished = true;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index c01b2576e8..5fa4e24f5e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@@ -43,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Tests
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner = null!;
- private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
[SetUpSteps]
public override void SetUpSteps()
@@ -77,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Tests
{
double finalCumulativeTrackerRotation = 0;
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
- double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
@@ -85,11 +82,6 @@ namespace osu.Game.Rulesets.Osu.Tests
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
});
- AddStep("retrieve spinner symbol rotation", () =>
- {
- finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
- spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
- });
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(spinner_start_time + 2500);
@@ -98,8 +90,6 @@ namespace osu.Game.Rulesets.Osu.Tests
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
// (5% relative to the final rotation value, but we're half-way through the spin).
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
- AddAssert("symbol rotation rewound",
- () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
@@ -107,8 +97,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
- AddAssert("is symbol rotation almost same",
- () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
}
@@ -122,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(5000);
AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0);
- AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
private Replay flip(Replay scoreReplay) => new Replay
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
new file mode 100644
index 0000000000..80e3af6cc0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
@@ -0,0 +1,149 @@
+// Copyright (c) ppy Pty Ltd . 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.Audio;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Skinning;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneTrianglesSpinnerRotation : TestSceneOsuPlayer
+ {
+ private const double spinner_start_time = 100;
+ private const double spinner_duration = 6000;
+
+ [Resolved]
+ private SkinManager skinManager { get; set; } = null!;
+
+ [Resolved]
+ private AudioManager audioManager { get; set; } = null!;
+
+ protected override bool Autoplay => true;
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
+
+ private DrawableSpinner drawableSpinner = null!;
+ private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
+
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("set triangles skin", () => skinManager.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+ AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
+ }
+
+ [Test]
+ public void TestSymbolMiddleRewindingRotation()
+ {
+ double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
+
+ addSeekStep(spinner_start_time + 5000);
+ AddStep("retrieve spinner symbol rotation", () =>
+ {
+ finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
+ spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
+ });
+
+ addSeekStep(spinner_start_time + 2500);
+ AddAssert("symbol rotation rewound",
+ () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
+
+ addSeekStep(spinner_start_time + 5000);
+ AddAssert("is symbol rotation almost same",
+ () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
+ }
+
+ [Test]
+ public void TestSymbolRotationDirection([Values(true, false)] bool clockwise)
+ {
+ if (clockwise)
+ transformReplay(flip);
+
+ addSeekStep(5000);
+ AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
+ }
+
+ private Replay flip(Replay scoreReplay) => new Replay
+ {
+ Frames = scoreReplay
+ .Frames
+ .Cast()
+ .Select(replayFrame =>
+ {
+ var flippedPosition = new Vector2(OsuPlayfield.BASE_SIZE.X - replayFrame.Position.X, replayFrame.Position.Y);
+ return new OsuReplayFrame(replayFrame.Time, flippedPosition, replayFrame.Actions.ToArray());
+ })
+ .Cast()
+ .ToList()
+ };
+
+ private void addSeekStep(double time)
+ {
+ AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
+ AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
+ }
+
+ private void transformReplay(Func replayTransformation) => AddStep("set replay", () =>
+ {
+ var drawableRuleset = this.ChildrenOfType().Single();
+ var score = drawableRuleset.ReplayScore;
+ var transformedScore = new Score
+ {
+ ScoreInfo = score.ScoreInfo,
+ Replay = replayTransformation.Invoke(score.Replay)
+ };
+ drawableRuleset.SetReplayScore(transformedScore);
+ });
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = spinner_start_time,
+ Duration = spinner_duration
+ },
+ }
+ };
+
+ private class ScoreExposedPlayer : TestPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ public ScoreExposedPlayer()
+ : base(false, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index c2973644cf..1eb1c85d93 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -1,10 +1,10 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
index 2ba856d014..dabbfcd2fb 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (!(currentObj.BaseObject is Spinner))
{
- double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
+ double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
cumulativeStrainTime += lastObj.StrainTime;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 33787da8f6..1e83d6d820 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -44,6 +44,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
+ if (mods.Any(m => m is OsuModTouchDevice))
+ {
+ aimRating = Math.Pow(aimRating, 0.8);
+ flashlightRating = Math.Pow(flashlightRating, 0.8);
+ }
+
if (mods.Any(h => h is OsuModRelax))
{
aimRating *= 0.9;
@@ -127,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
+ new OsuModTouchDevice(),
new OsuModDoubleTime(),
new OsuModHalfTime(),
new OsuModEasy(),
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index fb0eff5cb2..30b56ff769 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -88,12 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double rawAim = attributes.AimDifficulty;
-
- if (score.Mods.Any(m => m is OsuModTouchDevice))
- rawAim = Math.Pow(rawAim, 0.8);
-
- double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
+ double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -233,12 +228,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
- double rawFlashlight = attributes.FlashlightDifficulty;
-
- if (score.Mods.Any(m => m is OsuModTouchDevice))
- rawFlashlight = Math.Pow(rawFlashlight, 0.8);
-
- double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
+ double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index c24f78e430..94655f3cf7 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -303,11 +303,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
{
+ var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition));
+
+ Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - slider.Position;
+
for (int i = 0; i < controlPoints.Count; ++i)
{
var controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint))
- controlPoint.Position = dragStartPositions[i] + (e.MousePosition - e.MouseDownPosition);
+ controlPoint.Position = dragStartPositions[i] + movementDelta;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index e2f98c273e..dd5335a743 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -198,7 +198,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
- cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
+ var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
+ cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index eb69efd636..7c289b5b05 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -163,7 +163,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnDrag(DragEvent e)
{
if (placementControlPoint != null)
- placementControlPoint.Position = e.MousePosition - HitObject.Position;
+ {
+ var result = snapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition));
+ placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position;
+ }
}
protected override void OnMouseUp(MouseUpEvent e)
diff --git a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
index a7aca8257b..e4e8905722 100644
--- a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
+++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
@@ -18,7 +18,7 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
- public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset
+ public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset, IUpdatableByPlayfield
{
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
@@ -62,15 +62,18 @@ namespace osu.Game.Rulesets.Osu.Mods
gameplayClock = drawableRuleset.FrameStableClock;
}
+ public void Update(Playfield playfield)
+ {
+ if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
+ LastAcceptedAction = null;
+ }
+
protected abstract bool CheckValidNewAction(OsuAction action);
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
- {
- LastAcceptedAction = null;
return true;
- }
switch (action)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
index e624660410..f6622c268d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
@@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Hit them at the right size!";
- [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
- public override BindableNumber StartScale { get; } = new BindableFloat
+ public override BindableNumber StartScale { get; } = new BindableFloat(2)
{
MinValue = 1f,
MaxValue = 25f,
- Default = 2f,
- Value = 2f,
Precision = 0.1f,
};
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index e5a458488e..66f367c79b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -32,24 +32,16 @@ namespace osu.Game.Rulesets.Osu.Mods
Precision = default_follow_delay,
};
- [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableFloat SizeMultiplier { get; } = new BindableFloat
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 2f,
- Default = 1f,
- Value = 1f,
Precision = 0.1f
};
- [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
- public override BindableBool ComboBasedSize { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
- public override float DefaultFlashlightSize => 180;
+ public override float DefaultFlashlightSize => 200;
private OsuFlashlight flashlight = null!;
@@ -71,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Mods
followDelay = modFlashlight.FollowDelay.Value;
FlashlightSize = new Vector2(0, GetSizeFor(0));
+ FlashlightSmoothness = 1.4f;
}
public void OnSliderTrackingChange(ValueChangedEvent e)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
index b77c887cd3..3d066d3ada 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
@@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Hit them at the right size!";
- [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
- public override BindableNumber StartScale { get; } = new BindableFloat
+ public override BindableNumber StartScale { get; } = new BindableFloat(0.5f)
{
MinValue = 0f,
MaxValue = 0.99f,
- Default = 0.5f,
- Value = 0.5f,
Precision = 0.01f,
};
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
index 817f7b599c..2f84c30581 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -7,8 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Configuration;
-using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
@@ -22,15 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private PeriodTracker spinnerPeriods = null!;
- [SettingSource(
- "Hidden at combo",
- "The combo count at which the cursor becomes completely hidden",
- SettingControlType = typeof(SettingsSlider)
- )]
- public override BindableInt HiddenComboCount { get; } = new BindableInt
+ public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
- Default = 10,
- Value = 10,
MinValue = 0,
MaxValue = 50,
};
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 59984f9a7b..6f1206382a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -20,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
+ [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public abstract BindableNumber StartScale { get; }
protected virtual float EndScale => 1;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 056a325dce..618fcfe05d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -4,9 +4,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -25,6 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
+ [SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider))]
+ public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
+ {
+ MinValue = 1,
+ MaxValue = 10,
+ Precision = 0.1f
+ };
+
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random random = null!;
@@ -50,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (shouldStartNewSection(osuBeatmap, positionInfos, i))
{
- sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.0008f);
+ sectionOffset = getRandomOffset(0.0008f);
flowDirection = !flowDirection;
}
@@ -65,11 +76,11 @@ namespace osu.Game.Rulesets.Osu.Mods
float flowChangeOffset = 0;
// Offsets only the angle of the current hit object.
- float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
+ float oneTimeOffset = getRandomOffset(0.002f);
if (shouldApplyFlowChange(positionInfos, i))
{
- flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
+ flowChangeOffset = getRandomOffset(0.002f);
flowDirection = !flowDirection;
}
@@ -86,13 +97,36 @@ namespace osu.Game.Rulesets.Osu.Mods
osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
+ private float getRandomOffset(float stdDev)
+ {
+ // Range: [0.5, 2]
+ // Higher angle sharpness -> lower multiplier
+ float customMultiplier = (1.5f * AngleSharpness.MaxValue - AngleSharpness.Value) / (1.5f * AngleSharpness.MaxValue - AngleSharpness.Default);
+
+ return OsuHitObjectGenerationUtils.RandomGaussian(random, 0, stdDev * customMultiplier);
+ }
+
/// The target distance between the previous and the current .
/// The angle (in rad) by which the target angle should be offset.
/// Whether the relative angle should be positive or negative.
- private static float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
+ private float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
{
- float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310))) + 0.5 + offset);
+ // Range: [0.1, 1]
+ float angleSharpness = AngleSharpness.Value / AngleSharpness.MaxValue;
+ // Range: [0, 0.9]
+ float angleWideness = 1 - angleSharpness;
+
+ // Range: [-60, 30]
+ float customOffsetX = angleSharpness * 100 - 70;
+ // Range: [-0.075, 0.15]
+ float customOffsetY = angleWideness * 0.25f - 0.075f;
+
+ targetDistance += customOffsetX;
+ float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310 + customOffsetX))) + 0.5);
+ angle += offset + customOffsetY;
+
float relativeAngle = (float)Math.PI - angle;
+
return flowDirection ? -relativeAngle : relativeAngle;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index 861ad80b7f..406968ba08 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -53,11 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
- public Bindable Seed { get; } = new Bindable
- {
- Default = null,
- Value = null
- };
+ public Bindable Seed { get; } = new Bindable();
[SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")]
public Bindable Metronome { get; } = new BindableBool(true);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 23e5cb0ad7..23db29b9a6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
@@ -47,12 +48,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
}
+ private ShakeContainer shakeContainer;
+
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
scaleContainer = new Container
{
@@ -72,22 +75,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
- CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
+ shakeContainer = new ShakeContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- Scale = new Vector2(4),
+ Children = new Drawable[]
+ {
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ Scale = new Vector2(4),
+ }
+ }
}
}
},
- };
+ });
Size = HitArea.DrawSize;
@@ -123,6 +134,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ public override void Shake() => shakeContainer.Shake();
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
@@ -139,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
- Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
+ Shake();
return;
}
@@ -191,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut();
- // in the case of an early state change, the fade should be expedited to the current point in time.
- if (HitStateUpdateTime < HitObject.StartTime)
- ApproachCircle.FadeOut(50);
-
switch (state)
{
+ default:
+ ApproachCircle.FadeOut();
+ break;
+
case ArmedState.Idle:
HitArea.HitAction = null;
break;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 6e525071ca..d9d0d28477 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -6,17 +6,18 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
-using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Osu.Scoring;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableOsuHitObject : DrawableHitObject
+ public abstract class DrawableOsuHitObject : DrawableHitObject
{
public readonly IBindable PositionBindable = new Bindable();
public readonly IBindable StackHeightBindable = new Bindable();
@@ -34,8 +35,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public Func CheckHittable;
- private ShakeContainer shakeContainer;
-
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
@@ -45,12 +44,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void load()
{
Alpha = 0;
-
- base.AddInternal(shakeContainer = new ShakeContainer
- {
- ShakeDuration = 30,
- RelativeSizeAxes = Axes.Both
- });
}
protected override void OnApply()
@@ -73,18 +66,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
}
- // Forward all internal management to shakeContainer.
- // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690)
- protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable);
- protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren);
- protected override bool RemoveInternal(Drawable drawable, bool disposeImmediately) => shakeContainer.Remove(drawable, disposeImmediately);
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ // Dim should only be applied at a top level, as it will be implicitly applied to nested objects.
+ if (ParentHitObject == null)
+ {
+ // Of note, no one noticed this was missing for years, but it definitely feels like it should still exist.
+ // For now this is applied across all skins, and matches stable.
+ // For simplicity, dim colour is applied to the DrawableHitObject itself.
+ // We may need to make a nested container setup if this even causes a usage conflict (ie. with a mod).
+ this.FadeColour(new Color4(195, 195, 195, 255));
+ using (BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
+ this.FadeColour(Color4.White, 100);
+ }
+ }
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager;
- public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
+ ///
+ /// Shake the hit object in case it was clicked far too early or late (aka "note lock").
+ ///
+ public virtual void Shake() { }
///
/// Causes this to get missed, disregarding all conditions in implementations of .
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index d83f5df7a3..d58a435728 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SkinnableDrawable Body { get; private set; }
+ private ShakeContainer shakeContainer;
+
///
/// A target container which can be used to add top level elements to the slider's display.
/// Intended to be used for proxy purposes only.
@@ -74,17 +77,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
- Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
- tickContainer = new Container { RelativeSizeAxes = Axes.Both },
- repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
+ shakeContainer = new ShakeContainer
+ {
+ ShakeDuration = 30,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
+ tickContainer = new Container { RelativeSizeAxes = Axes.Both },
+ repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
+ }
+ },
+ // slider head is not included in shake as it handles hit detection, and handles its own shaking.
headContainer = new Container { RelativeSizeAxes = Axes.Both },
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
Ball,
slidingSample = new PausableSkinnableSound { Looping = true }
- };
+ });
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
@@ -109,6 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PathVersion.BindTo(HitObject.Path.Version);
}
+ public override void Shake() => shakeContainer.Shake();
+
protected override void OnFree()
{
base.OnFree();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index 70b1bd225f..80b9544e5b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindTo(DrawableSlider.PathVersion);
- OnShake = DrawableSlider.Shake;
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
}
@@ -96,9 +95,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
- public Action OnShake;
-
- public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
+ public override void Shake()
+ {
+ base.Shake();
+ DrawableSlider.Shake();
+ }
private void updatePosition()
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 1bddc603ac..7b9c0c7e40 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- InternalChild = scaleContainer = new Container
+ AddInternal(scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
Arrow = new ReverseArrowPiece(),
}
- };
+ });
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index df3a12fe33..063d297f5a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
scaleContainer = new Container
{
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
}
},
- };
+ });
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
index 3ffbe68b98..4bd98fc8b2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Origin = Anchor.Centre;
- InternalChild = scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
+ AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
{
Masking = true,
Origin = Anchor.Centre,
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- };
+ });
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
index 6a15463a32..4975ca1248 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
}
- protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
+ public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
///
/// Apply a judgement result.
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index 569e9b7c1c..676ff62455 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// This is so on repeats ticks don't appear too late to be visually processed by the player.
offset = 200;
else
- offset = TimeFadeIn * 0.66f;
+ offset = TimePreempt * 0.66f;
TimePreempt = (StartTime - SpanStartTime) / 2 + offset;
}
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index 12256e93d0..dec965e567 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -80,6 +80,9 @@ namespace osu.Game.Rulesets.Osu
LeftButton,
[Description("Right button")]
- RightButton
+ RightButton,
+
+ [Description("Smoke")]
+ Smoke,
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 7a11fc1755..69df12ff6d 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -27,6 +27,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Rulesets.Osu.Skinning.Argon;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Osu.UI;
@@ -58,6 +59,7 @@ namespace osu.Game.Rulesets.Osu
{
new KeyBinding(InputKey.Z, OsuAction.LeftButton),
new KeyBinding(InputKey.X, OsuAction.RightButton),
+ new KeyBinding(InputKey.C, OsuAction.Smoke),
new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton),
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
};
@@ -238,6 +240,9 @@ namespace osu.Game.Rulesets.Osu
{
case LegacySkin:
return new OsuLegacySkinTransformer(skin);
+
+ case ArgonSkin:
+ return new OsuArgonSkinTransformer(skin);
}
return null;
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index fcf079b6aa..4248cce55a 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu
SliderBall,
SliderBody,
SpinnerBody,
+ CursorSmoke,
ApproachCircle,
}
}
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
index 85060261fe..8082c5aef4 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Position = currentFrame.Position;
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton);
+ if (currentFrame.Smoke) Actions.Add(OsuAction.Smoke);
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
@@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Replays
state |= ReplayButtonState.Left1;
if (Actions.Contains(OsuAction.RightButton))
state |= ReplayButtonState.Right1;
+ if (Actions.Contains(OsuAction.Smoke))
+ state |= ReplayButtonState.Smoke;
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
index 05fbac625e..6f55e1790f 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
@@ -1,20 +1,23 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuHitWindows : HitWindows
{
+ ///
+ /// osu! ruleset has a fixed miss window regardless of difficulty settings.
+ ///
+ public const double MISS_WINDOW = 400;
+
private static readonly DifficultyRange[] osu_ranges =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
- new DifficultyRange(HitResult.Miss, 400, 400, 400),
+ new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
};
public override bool IsHitResultAllowed(HitResult result)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs
new file mode 100644
index 0000000000..446f3c83ae
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Osu.UI.Cursor;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonCursor : OsuCursorSprite
+ {
+ public ArgonCursor()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ InternalChildren = new[]
+ {
+ ExpandTarget = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 6,
+ BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.4f,
+ Colour = Colour4.FromHex("FC618F").Darken(0.6f),
+ },
+ new CircularContainer
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 2,
+ BorderColour = Color4.White.Opacity(0.8f),
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ },
+ },
+ },
+ },
+ new Circle
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Scale = new Vector2(0.2f),
+ Colour = new Color4(255, 255, 255, 255),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 20,
+ Colour = new Color4(171, 255, 255, 100),
+ },
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
new file mode 100644
index 0000000000..9bb3122a3b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . 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.Textures;
+using osu.Game.Rulesets.Osu.UI.Cursor;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonCursorTrail : CursorTrail
+ {
+ protected override float IntervalMultiplier => 0.4f;
+
+ protected override float FadeExponent => 4;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ Texture = textures.Get(@"Cursor/cursortrail");
+ Scale = new Vector2(0.8f / Texture.ScaleAdjust);
+
+ Blending = BlendingParameters.Additive;
+
+ Alpha = 0.8f;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
new file mode 100644
index 0000000000..83c5f6295a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . 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.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonFollowCircle : FollowCircle
+ {
+ public ArgonFollowCircle()
+ {
+ InternalChild = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 4,
+ BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ Blending = BlendingParameters.Additive,
+ Child = new Box
+ {
+ Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.3f,
+ }
+ };
+ }
+
+ protected override void OnSliderPress()
+ {
+ const float duration = 300f;
+
+ if (Precision.AlmostEquals(0, Alpha))
+ this.ScaleTo(1);
+
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA, duration, Easing.OutQuint)
+ .FadeIn(duration, Easing.OutQuint);
+ }
+
+ protected override void OnSliderRelease()
+ {
+ const float duration = 150;
+
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.2f, duration, Easing.OutQuint)
+ .FadeTo(0, duration, Easing.OutQuint);
+ }
+
+ protected override void OnSliderEnd()
+ {
+ const float duration = 300;
+
+ this.ScaleTo(1, duration, Easing.OutQuint)
+ .FadeOut(duration / 2, Easing.OutQuint);
+ }
+
+ protected override void OnSliderTick()
+ {
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
+ .Then()
+ .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
+ }
+
+ protected override void OnSliderBreak()
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
new file mode 100644
index 0000000000..47dae3c30a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . 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.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonFollowPoint : CompositeDrawable
+ {
+ public ArgonFollowPoint()
+ {
+ Blending = BlendingParameters.Additive;
+
+ Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41"));
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.ChevronRight,
+ Size = new Vector2(8),
+ Colour = OsuColour.Gray(0.2f),
+ },
+ new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.ChevronRight,
+ Size = new Vector2(8),
+ X = 4,
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
new file mode 100644
index 0000000000..b08b7b4e85
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
@@ -0,0 +1,171 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; } = null!;
+
+ private RingExplosion? ringExplosion;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ArgonJudgementPiece(HitResult result)
+ {
+ Result = result;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(5, 0),
+ Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
+ },
+ };
+
+ if (Result.IsHit())
+ {
+ AddInternal(ringExplosion = new RingExplosion(Result)
+ {
+ Colour = colours.ForHitResult(Result),
+ });
+ }
+ }
+
+ ///
+ /// Plays the default animation for this judgement piece.
+ ///
+ ///
+ /// The base implementation only handles fade (for all result types) and misses.
+ /// Individual rulesets are recommended to implement their appropriate hit animations.
+ ///
+ public virtual void PlayAnimation()
+ {
+ switch (Result)
+ {
+ default:
+ JudgementText
+ .ScaleTo(Vector2.One)
+ .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
+ break;
+
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ this.MoveTo(Vector2.Zero);
+ this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, 800, Easing.InQuint);
+ break;
+ }
+
+ this.FadeOutFromOne(800);
+
+ ringExplosion?.PlayAnimation();
+ }
+
+ public Drawable? GetAboveHitObjectsProxiedContent() => null;
+
+ private class RingExplosion : CompositeDrawable
+ {
+ private readonly float travel = 52;
+
+ public RingExplosion(HitResult result)
+ {
+ const float thickness = 4;
+
+ const float small_size = 9;
+ const float large_size = 14;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Blending = BlendingParameters.Additive;
+
+ int countSmall = 0;
+ int countLarge = 0;
+
+ switch (result)
+ {
+ case HitResult.Meh:
+ countSmall = 3;
+ travel *= 0.3f;
+ break;
+
+ case HitResult.Ok:
+ case HitResult.Good:
+ countSmall = 4;
+ travel *= 0.6f;
+ break;
+
+ case HitResult.Great:
+ case HitResult.Perfect:
+ countSmall = 4;
+ countLarge = 4;
+ break;
+ }
+
+ for (int i = 0; i < countSmall; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
+
+ for (int i = 0; i < countLarge; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
+ }
+
+ public void PlayAnimation()
+ {
+ foreach (var c in InternalChildren)
+ {
+ const float start_position_ratio = 0.3f;
+
+ float direction = RNG.NextSingle(0, 360);
+ float distance = RNG.NextSingle(travel / 2, travel);
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance * start_position_ratio,
+ MathF.Sin(direction) * distance * start_position_ratio
+ ));
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance,
+ MathF.Sin(direction) * distance
+ ), 600, Easing.OutQuint);
+ }
+
+ this.FadeOutFromOne(1000, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
new file mode 100644
index 0000000000..ffdcba3cdb
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -0,0 +1,226 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonMainCirclePiece : CompositeDrawable
+ {
+ public const float BORDER_THICKNESS = (OsuHitObject.OBJECT_RADIUS * 2) * (2f / 58);
+
+ public const float GRADIENT_THICKNESS = BORDER_THICKNESS * 2.5f;
+
+ public const float OUTER_GRADIENT_SIZE = (OsuHitObject.OBJECT_RADIUS * 2) - BORDER_THICKNESS * 4;
+
+ public const float INNER_GRADIENT_SIZE = OUTER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
+ public const float INNER_FILL_SIZE = INNER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
+
+ private readonly Circle outerFill;
+ private readonly Circle outerGradient;
+ private readonly Circle innerGradient;
+ private readonly Circle innerFill;
+
+ private readonly RingPiece border;
+ private readonly OsuSpriteText number;
+
+ private readonly IBindable accentColour = new Bindable();
+ private readonly IBindable indexInCurrentCombo = new Bindable();
+ private readonly FlashPiece flash;
+
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; } = null!;
+
+ public ArgonMainCirclePiece(bool withOuterFill)
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ InternalChildren = new Drawable[]
+ {
+ outerFill = new Circle // renders white outer border and dark fill
+ {
+ Size = Size,
+ Alpha = withOuterFill ? 1 : 0,
+ },
+ outerGradient = new Circle // renders the outer bright gradient
+ {
+ Size = new Vector2(OUTER_GRADIENT_SIZE),
+ Alpha = 1,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ innerGradient = new Circle // renders the inner bright gradient
+ {
+ Size = new Vector2(INNER_GRADIENT_SIZE),
+ Alpha = 1,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ innerFill = new Circle // renders the inner dark fill
+ {
+ Size = new Vector2(INNER_FILL_SIZE),
+ Alpha = 1,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ number = new OsuSpriteText
+ {
+ Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = -2,
+ Text = @"1",
+ },
+ flash = new FlashPiece(),
+ border = new RingPiece(BORDER_THICKNESS),
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
+
+ accentColour.BindTo(drawableObject.AccentColour);
+ indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ accentColour.BindValueChanged(colour =>
+ {
+ outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
+ outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
+ innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
+ flash.Colour = colour.NewValue;
+ }, true);
+
+ indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
+
+ drawableObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableObject, drawableObject.State.Value);
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ // Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec.
+ const double fade_out_time = 800;
+
+ const double flash_in_duration = 150;
+ const double resize_duration = 400;
+
+ const float shrink_size = 0.8f;
+
+ // Animating with the number present is distracting.
+ // The number disappearing is hidden by the bright flash.
+ number.FadeOut(flash_in_duration / 2);
+
+ // The fill layers add too much noise during the explosion animation.
+ // They will be hidden by the additive effects anyway.
+ outerFill.FadeOut(flash_in_duration, Easing.OutQuint);
+ innerFill.FadeOut(flash_in_duration, Easing.OutQuint);
+
+ // The inner-most gradient should actually be resizing, but is only visible for
+ // a few milliseconds before it's hidden by the flash, so it's pointless overhead to bother with it.
+ innerGradient.FadeOut(flash_in_duration, Easing.OutQuint);
+
+ // The border is always white, but after hit it gets coloured by the skin/beatmap's colouring.
+ // A gradient is applied to make the border less prominent over the course of the animation.
+ // Without this, the border dominates the visual presence of the explosion animation in a bad way.
+ border.TransformTo(nameof
+ (BorderColour), ColourInfo.GradientVertical(
+ accentColour.Value.Opacity(0.5f),
+ accentColour.Value.Opacity(0)), fade_out_time);
+
+ // The outer ring shrinks immediately, but accounts for its thickness so it doesn't overlap the inner
+ // gradient layers.
+ border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf);
+
+ // The outer gradient is resize with a slight delay from the border.
+ // This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
+ using (BeginDelayedSequence(flash_in_duration / 12))
+ {
+ outerGradient.ResizeTo(outerGradient.Size * shrink_size, resize_duration, Easing.OutElasticHalf);
+ outerGradient
+ .FadeColour(Color4.White, 80)
+ .Then()
+ .FadeOut(flash_in_duration);
+ }
+
+ // The flash layer starts white to give the wanted brightness, but is almost immediately
+ // recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
+ // but works well enough with the colour fade.
+ flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
+ flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
+
+ this.FadeOut(fade_out_time, Easing.OutQuad);
+ break;
+ }
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableObject.IsNotNull())
+ drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+
+ private class FlashPiece : Circle
+ {
+ public FlashPiece()
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Alpha = 0;
+ Blending = BlendingParameters.Additive;
+
+ // The edge effect provides the fill due to not being rendered hollow.
+ Child.Alpha = 0;
+ Child.AlwaysPresent = true;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Colour,
+ Radius = OsuHitObject.OBJECT_RADIUS * 1.2f,
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
new file mode 100644
index 0000000000..9d44db3614
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonReverseArrow : CompositeDrawable
+ {
+ private Bindable accentColour = null!;
+
+ private SpriteIcon icon = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject hitObject)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+
+ InternalChildren = new Drawable[]
+ {
+ new Circle
+ {
+ Size = new Vector2(40, 20),
+ Colour = Color4.White,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ icon = new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.AngleDoubleRight,
+ Size = new Vector2(16),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+
+ accentColour = hitObject.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
new file mode 100644
index 0000000000..3df9edd225
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
@@ -0,0 +1,109 @@
+// Copyright (c) ppy Pty Ltd . 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.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSliderBall : CircularContainer
+ {
+ private readonly Box fill;
+ private readonly SpriteIcon icon;
+
+ private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
+
+ public ArgonSliderBall()
+ {
+ Size = new Vector2(ArgonMainCirclePiece.OUTER_GRADIENT_SIZE);
+
+ Masking = true;
+
+ BorderThickness = ArgonMainCirclePiece.GRADIENT_THICKNESS;
+ BorderColour = Color4.White;
+
+ InternalChildren = new Drawable[]
+ {
+ fill = new Box
+ {
+ Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ icon = new SpriteIcon
+ {
+ Size = new Vector2(48),
+ Scale = defaultIconScale,
+ Icon = FontAwesome.Solid.AngleRight,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (parentObject != null)
+ {
+ parentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(parentObject, parentObject.State.Value);
+ }
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which in this case have no visual impact (due to
+ // instant fade) but may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ const float duration = 200;
+ const float icon_scale = 0.9f;
+
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
+ {
+ this.FadeInFromZero(duration, Easing.OutQuint);
+ icon.ScaleTo(0).Then().ScaleTo(defaultIconScale, duration, Easing.OutElasticHalf);
+ }
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ this.FadeOut(duration, Easing.OutQuint);
+ icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ //undo rotation on layers which should not be rotated.
+ float appliedRotation = Parent.Rotation;
+
+ fill.Rotation = -appliedRotation;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (parentObject != null)
+ parentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
new file mode 100644
index 0000000000..e1642d126d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSliderBody : PlaySliderBody
+ {
+ protected override void LoadComplete()
+ {
+ const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
+
+ base.LoadComplete();
+
+ AccentColourBindable.BindValueChanged(accent => BorderColour = accent.NewValue, true);
+ ScaleBindable.BindValueChanged(scale => PathRadius = path_radius * scale.NewValue, true);
+
+ // This border size thing is kind of weird, hey.
+ const float intended_thickness = ArgonMainCirclePiece.GRADIENT_THICKNESS / path_radius;
+
+ BorderSize = intended_thickness / Default.DrawableSliderPath.BORDER_PORTION;
+ }
+
+ protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
+
+ private class DrawableSliderPath : Default.DrawableSliderPath
+ {
+ protected override Color4 ColourAt(float position)
+ {
+ if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion)
+ return BorderColour;
+
+ return AccentColour.Darken(4);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs
new file mode 100644
index 0000000000..4c6b9a2f17
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSliderScorePoint : CircularContainer
+ {
+ private Bindable accentColour = null!;
+
+ private const float size = 12;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject hitObject)
+ {
+ Masking = true;
+ Origin = Anchor.Centre;
+ Size = new Vector2(size);
+ BorderThickness = 3;
+ BorderColour = Color4.White;
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ AlwaysPresent = true,
+ Alpha = 0,
+ };
+
+ accentColour = hitObject.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(accent => BorderColour = accent.NewValue, true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
new file mode 100644
index 0000000000..95438e9588
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
@@ -0,0 +1,146 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Globalization;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinner : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner = null!;
+
+ private OsuSpriteText bonusCounter = null!;
+
+ private Container spmContainer = null!;
+ private OsuSpriteText spmCounter = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ bonusCounter = new OsuSpriteText
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Default.With(size: 24),
+ Y = -120,
+ },
+ new ArgonSpinnerDisc
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ bonusCounter = new OsuSpriteText
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.Bold),
+ Y = -100,
+ },
+ spmContainer = new Container
+ {
+ Alpha = 0f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 60,
+ Children = new[]
+ {
+ spmCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"0",
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.SemiBold)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"SPINS PER MINUTE",
+ Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
+ Y = 30
+ }
+ }
+ }
+ };
+ }
+
+ private IBindable gainedBonus = null!;
+ private IBindable spinsPerMinute = null!;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
+ gainedBonus.BindValueChanged(bonus =>
+ {
+ bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
+ bonusCounter.FadeOutFromOne(1500);
+ bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
+ });
+
+ spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
+ fadeCounterOnTimeStart();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ fadeCounterOnTimeStart();
+ }
+
+ private void fadeCounterOnTimeStart()
+ {
+ if (drawableSpinner.Result?.TimeStarted is double startTime)
+ {
+ using (BeginAbsoluteSequence(startTime))
+ spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner.IsNotNull())
+ drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
new file mode 100644
index 0000000000..4669b5b913
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
@@ -0,0 +1,247 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerDisc : CompositeDrawable
+ {
+ private const float initial_scale = 1f;
+ private const float idle_alpha = 0.2f;
+ private const float tracking_alpha = 0.4f;
+
+ private const float idle_centre_size = 80f;
+ private const float tracking_centre_size = 40f;
+
+ private DrawableSpinner drawableSpinner = null!;
+
+ private readonly BindableBool complete = new BindableBool();
+
+ private int wholeRotationCount;
+
+ private bool checkNewRotationCount
+ {
+ get
+ {
+ int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
+
+ if (wholeRotationCount == rotations) return false;
+
+ wholeRotationCount = rotations;
+ return true;
+ }
+ }
+
+ private Container disc = null!;
+ private Container centre = null!;
+ private CircularContainer fill = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+
+ // we are slightly bigger than our parent, to clip the top and bottom of the circle
+ // this should probably be revisited when scaled spinners are a thing.
+ Scale = new Vector2(initial_scale);
+
+ InternalChildren = new Drawable[]
+ {
+ disc = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ fill = new CircularContainer
+ {
+ Name = @"Fill",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Colour4.FromHex("FC618F").Opacity(1f),
+ Radius = 40,
+ },
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0f,
+ AlwaysPresent = true,
+ }
+ },
+ new CircularContainer
+ {
+ Name = @"Ring",
+ Masking = true,
+ BorderColour = Color4.White,
+ BorderThickness = 5,
+ RelativeSizeAxes = Axes.Both,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ }
+ },
+ new ArgonSpinnerTicks(),
+ }
+ },
+ centre = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(idle_centre_size),
+ Children = new[]
+ {
+ new RingPiece(10)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.8f),
+ },
+ new RingPiece(3)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(1f),
+ }
+ },
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted;
+
+ if (complete.Value)
+ {
+ if (checkNewRotationCount)
+ {
+ fill.FinishTransforms(false, nameof(Alpha));
+ fill
+ .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
+ .Then()
+ .FadeTo(tracking_alpha, 250, Easing.OutQuint);
+ }
+ }
+ else
+ {
+ fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
+ }
+
+ if (centre.Width == idle_centre_size && drawableSpinner.Result?.TimeStarted != null)
+ updateCentrePieceSize();
+
+ const float initial_fill_scale = 0.1f;
+ float targetScale = initial_fill_scale + (0.98f - initial_fill_scale) * drawableSpinner.Progress;
+
+ fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
+ disc.Rotation = drawableSpinner.RotationTracker.Rotation;
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ Spinner spinner = drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
+ {
+ this.ScaleTo(initial_scale);
+ this.RotateTo(0);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
+ {
+ // constant ambient rotation to give the spinner "spinning" character.
+ this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+ }
+
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
+ this.RotateTo(Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
+ }
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
+ {
+ centre.ScaleTo(0);
+ disc.ScaleTo(0);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
+ {
+ centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
+ disc.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
+ {
+ centre.ScaleTo(0.8f, spinner.TimePreempt / 2, Easing.OutQuint);
+ disc.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ }
+ }
+ }
+
+ if (drawableSpinner.Result?.TimeStarted != null)
+ updateCentrePieceSize();
+ }
+
+ private void updateCentrePieceSize()
+ {
+ Debug.Assert(drawableSpinner.Result?.TimeStarted != null);
+
+ Spinner spinner = drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(drawableSpinner.Result.TimeStarted.Value))
+ centre.ResizeTo(new Vector2(tracking_centre_size), spinner.TimePreempt / 2, Easing.OutQuint);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner.IsNotNull())
+ drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
new file mode 100644
index 0000000000..0203432088
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerTicks : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Origin = Anchor.Centre;
+ Anchor = Anchor.Centre;
+ RelativeSizeAxes = Axes.Both;
+
+ const float count = 25;
+
+ for (float i = 0; i < count; i++)
+ {
+ AddInternal(new CircularContainer
+ {
+ RelativePositionAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = 5,
+ BorderColour = Color4.White,
+ BorderThickness = 2f,
+ Size = new Vector2(30, 5),
+ Origin = Anchor.Centre,
+ Position = new Vector2(
+ 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.75f,
+ 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.75f
+ ),
+ Rotation = -i / count * 360 - 120,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Colour4.White.Opacity(0.2f),
+ Radius = 30,
+ },
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ }
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
new file mode 100644
index 0000000000..bf507db50c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
@@ -0,0 +1,68 @@
+// Copyright (c) ppy Pty Ltd . 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.Scoring;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class OsuArgonSkinTransformer : SkinTransformer
+ {
+ public OsuArgonSkinTransformer(ISkin skin)
+ : base(skin)
+ {
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponent component)
+ {
+ switch (component)
+ {
+ case GameplaySkinComponent resultComponent:
+ return new ArgonJudgementPiece(resultComponent.Component);
+
+ case OsuSkinComponent osuComponent:
+ // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
+ switch (osuComponent.Component)
+ {
+ case OsuSkinComponents.HitCircle:
+ return new ArgonMainCirclePiece(true);
+
+ case OsuSkinComponents.SliderHeadHitCircle:
+ return new ArgonMainCirclePiece(false);
+
+ case OsuSkinComponents.SliderBody:
+ return new ArgonSliderBody();
+
+ case OsuSkinComponents.SliderBall:
+ return new ArgonSliderBall();
+
+ case OsuSkinComponents.SliderFollowCircle:
+ return new ArgonFollowCircle();
+
+ case OsuSkinComponents.SliderScorePoint:
+ return new ArgonSliderScorePoint();
+
+ case OsuSkinComponents.SpinnerBody:
+ return new ArgonSpinner();
+
+ case OsuSkinComponents.ReverseArrow:
+ return new ArgonReverseArrow();
+
+ case OsuSkinComponents.FollowPoint:
+ return new ArgonFollowPoint();
+
+ case OsuSkinComponents.Cursor:
+ return new ArgonCursor();
+
+ case OsuSkinComponents.CursorTrail:
+ return new ArgonCursorTrail();
+ }
+
+ break;
+ }
+
+ return base.GetDrawableComponent(component);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs
new file mode 100644
index 0000000000..27a2dc3960
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . 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.Textures;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public class DefaultSmokeSegment : SmokeSegment
+ {
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ // ISkinSource doesn't currently fallback to global textures.
+ // We might want to change this in the future if the intention is to allow the user to skin this as per legacy skins.
+ Texture = textures.Get("Gameplay/osu/cursor-smoke");
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs
index 94f93807d4..e3a83a9280 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs
@@ -10,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class DrawableSliderPath : SmoothPath
{
- protected const float BORDER_PORTION = 0.128f;
- protected const float GRADIENT_PORTION = 1 - BORDER_PORTION;
+ public const float BORDER_PORTION = 0.128f;
+ public const float GRADIENT_PORTION = 1 - BORDER_PORTION;
private const float border_max_size = 8f;
private const float border_min_size = 0f;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
index 83f7bb8904..6c422cf127 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
@@ -16,9 +16,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class PlaySliderBody : SnakingSliderBody
{
- private IBindable scaleBindable;
+ protected IBindable ScaleBindable { get; private set; } = null!;
+
+ protected IBindable AccentColourBindable { get; private set; } = null!;
+
private IBindable pathVersion;
- private IBindable accentColour;
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
@@ -30,14 +32,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
var drawableSlider = (DrawableSlider)drawableObject;
- scaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
- scaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
+ ScaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
+ ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
pathVersion = drawableSlider.PathVersion.GetBoundCopy();
pathVersion.BindValueChanged(_ => Refresh());
- accentColour = drawableObject.AccentColour.GetBoundCopy();
- accentColour.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
+ AccentColourBindable = drawableObject.AccentColour.GetBoundCopy();
+ AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs
index b941a86171..e813a7e274 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -14,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class RingPiece : CircularContainer
{
- public RingPiece()
+ public RingPiece(float thickness = 9)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre;
Masking = true;
- BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
+ BorderThickness = thickness;
BorderColour = Color4.White;
Child = new Box
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index d5cc469ca9..22944becf3 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
var topProvider = source.FindProvider(s => s.GetTexture("spinner-top") != null);
- if (topProvider is LegacySkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
+ if (topProvider is ISkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
{
AddInternal(ApproachCircle = new Sprite
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
new file mode 100644
index 0000000000..c9c7e86e86
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+ public class LegacySmokeSegment : SmokeSegment
+ {
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ base.LoadComplete();
+
+ Texture = skin.GetTexture("cursor-smoke");
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 885a2c12fb..b778bc21d1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
+ case OsuSkinComponents.CursorSmoke:
+ if (GetTexture("cursor-smoke") != null)
+ return new LegacySmokeSegment();
+
+ return null;
+
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null;
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
new file mode 100644
index 0000000000..6c998e244c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -0,0 +1,366 @@
+// Copyright (c) ppy Pty Ltd . 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.Diagnostics;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Rendering.Vertices;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
+ {
+ private const int max_point_count = 18_000;
+
+ // fade anim values
+ private const double initial_fade_out_duration = 4000;
+
+ private const double re_fade_in_speed = 3;
+ private const double re_fade_in_duration = 50;
+
+ private const double final_fade_out_speed = 2;
+ private const double final_fade_out_duration = 8000;
+
+ private const float initial_alpha = 0.6f;
+ private const float re_fade_in_alpha = 1f;
+
+ private readonly int rotationSeed = RNG.Next();
+
+ // scale anim values
+ private const double scale_duration = 1200;
+
+ private const float initial_scale = 0.65f;
+ private const float final_scale = 1f;
+
+ // rotation anim values
+ private const double rotation_duration = 500;
+
+ private const float max_rotation = 0.25f;
+
+ public IShader? TextureShader { get; private set; }
+ public IShader? RoundedTextureShader { get; private set; }
+
+ protected Texture? Texture { get; set; }
+
+ private float radius => Texture?.DisplayWidth * 0.165f ?? 3;
+
+ protected readonly List SmokePoints = new List();
+
+ private float pointInterval => radius * 7f / 8;
+
+ private double smokeStartTime { get; set; } = double.MinValue;
+
+ private double smokeEndTime { get; set; } = double.MaxValue;
+
+ private float totalDistance;
+ private Vector2? lastPosition;
+
+ [BackgroundDependencyLoader]
+ private void load(ShaderManager shaders)
+ {
+ RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ RelativeSizeAxes = Axes.Both;
+
+ LifetimeStart = smokeStartTime = Time.Current;
+
+ totalDistance = pointInterval;
+ }
+
+ private Vector2 nextPointDirection()
+ {
+ float angle = RNG.NextSingle(0, 2 * MathF.PI);
+ return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
+ }
+
+ public void AddPosition(Vector2 position, double time)
+ {
+ lastPosition ??= position;
+
+ float delta = (position - (Vector2)lastPosition).LengthFast;
+ totalDistance += delta;
+ int count = (int)(totalDistance / pointInterval);
+
+ if (count > 0)
+ {
+ Vector2 increment = position - (Vector2)lastPosition;
+ increment.NormalizeFast();
+
+ Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
+ increment *= pointInterval;
+
+ if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
+ {
+ int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
+ SmokePoints.RemoveRange(index, SmokePoints.Count - index);
+ }
+
+ totalDistance %= pointInterval;
+
+ for (int i = 0; i < count; i++)
+ {
+ SmokePoints.Add(new SmokePoint
+ {
+ Position = pointPos,
+ Time = time,
+ Direction = nextPointDirection(),
+ });
+
+ pointPos += increment;
+ }
+
+ Invalidate(Invalidation.DrawNode);
+ }
+
+ lastPosition = position;
+
+ if (SmokePoints.Count >= max_point_count)
+ FinishDrawing(time);
+ }
+
+ public void FinishDrawing(double time)
+ {
+ smokeEndTime = time;
+
+ double initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, smokeEndTime - smokeStartTime);
+ LifetimeEnd = smokeEndTime + final_fade_out_duration + initialFadeOutDurationTrunc / re_fade_in_speed + initialFadeOutDurationTrunc / final_fade_out_speed;
+ }
+
+ protected override DrawNode CreateDrawNode() => new SmokeDrawNode(this);
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Invalidate(Invalidation.DrawNode);
+ }
+
+ protected struct SmokePoint
+ {
+ public Vector2 Position;
+ public double Time;
+ public Vector2 Direction;
+
+ public struct UpperBoundComparer : IComparer
+ {
+ public int Compare(SmokePoint x, SmokePoint target)
+ {
+ // By returning -1 when the target value is equal to x, guarantees that the
+ // element at BinarySearch's returned index will always be the first element
+ // larger. Since 0 is never returned, the target is never "found", so the return
+ // value will be the index's complement.
+
+ return x.Time > target.Time ? 1 : -1;
+ }
+ }
+ }
+
+ protected class SmokeDrawNode : TexturedShaderDrawNode
+ {
+ protected new SmokeSegment Source => (SmokeSegment)base.Source;
+
+ protected double SmokeStartTime { get; private set; }
+ protected double SmokeEndTime { get; private set; }
+ protected double CurrentTime { get; private set; }
+
+ private readonly List points = new List();
+ private IVertexBatch? quadBatch;
+ private float radius;
+ private Vector2 drawSize;
+ private Texture? texture;
+
+ // anim calculation vars (color, scale, direction)
+ private double initialFadeOutDurationTrunc;
+ private double firstVisiblePointTime;
+
+ private double initialFadeOutTime;
+ private double reFadeInTime;
+ private double finalFadeOutTime;
+
+ private Random rotationRNG = new Random();
+
+ public SmokeDrawNode(ITexturedShaderDrawable source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ points.Clear();
+ points.AddRange(Source.SmokePoints);
+
+ radius = Source.radius;
+ drawSize = Source.DrawSize;
+ texture = Source.Texture;
+
+ SmokeStartTime = Source.smokeStartTime;
+ SmokeEndTime = Source.smokeEndTime;
+ CurrentTime = Source.Clock.CurrentTime;
+
+ rotationRNG = new Random(Source.rotationSeed);
+
+ initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
+ firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
+
+ initialFadeOutTime = CurrentTime;
+ reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
+ finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
+ }
+
+ public sealed override void Draw(IRenderer renderer)
+ {
+ base.Draw(renderer);
+
+ if (points.Count == 0)
+ return;
+
+ quadBatch ??= renderer.CreateQuadBatch(max_point_count / 10, 10);
+ texture ??= renderer.WhitePixel;
+ RectangleF textureRect = texture.GetTextureRect();
+
+ var shader = GetAppropriateShader(renderer);
+
+ renderer.SetBlend(BlendingParameters.Additive);
+ renderer.PushLocalMatrix(DrawInfo.Matrix);
+
+ shader.Bind();
+ texture.Bind();
+
+ foreach (var point in points)
+ drawPointQuad(point, textureRect);
+
+ shader.Unbind();
+ renderer.PopLocalMatrix();
+ }
+
+ protected Color4 ColourAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
+ ? ((SRGBColour)DrawColourInfo.Colour).Linear
+ : DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, drawSize)).Linear;
+
+ protected virtual Color4 PointColour(SmokePoint point)
+ {
+ var color = Color4.White;
+
+ double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
+
+ if (timeDoingInitialFadeOut > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
+ color.A = (1 - fraction) * initial_alpha;
+ }
+
+ if (color.A > 0)
+ {
+ double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
+ double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
+
+ if (timeDoingFinalFadeOut > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
+ fraction = MathF.Pow(fraction, 5);
+ color.A = (1 - fraction) * re_fade_in_alpha;
+ }
+ else if (timeDoingReFadeIn > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ }
+ }
+
+ return color;
+ }
+
+ protected virtual float PointScale(SmokePoint point)
+ {
+ double timeDoingScale = CurrentTime - point.Time;
+ float fraction = Math.Clamp((float)(timeDoingScale / scale_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ return fraction * (final_scale - initial_scale) + initial_scale;
+ }
+
+ protected virtual Vector2 PointDirection(SmokePoint point)
+ {
+ float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
+ float finalAngle = initialAngle + nextRotation();
+
+ double timeDoingRotation = CurrentTime - point.Time;
+ float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ float angle = fraction * (finalAngle - initialAngle) + initialAngle;
+
+ return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
+ }
+
+ private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1);
+
+ private void drawPointQuad(SmokePoint point, RectangleF textureRect)
+ {
+ Debug.Assert(quadBatch != null);
+
+ var colour = PointColour(point);
+ float scale = PointScale(point);
+ var dir = PointDirection(point);
+ var ortho = dir.PerpendicularLeft;
+
+ if (colour.A == 0 || scale == 0)
+ return;
+
+ var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
+ var localTopRight = point.Position + (radius * scale * (-ortho + dir));
+ var localBotLeft = point.Position + (radius * scale * (ortho - dir));
+ var localBotRight = point.Position + (radius * scale * (ortho + dir));
+
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localTopLeft,
+ TexturePosition = textureRect.TopLeft,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
+ });
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localTopRight,
+ TexturePosition = textureRect.TopRight,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
+ });
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localBotRight,
+ TexturePosition = textureRect.BottomRight,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
+ });
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localBotLeft,
+ TexturePosition = textureRect.BottomLeft,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localBotLeft), colour),
+ });
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ quadBatch?.Dispose();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index fc2ba8ea2f..2e67e91460 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
+ new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both },
diff --git a/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
new file mode 100644
index 0000000000..beba834e88
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
@@ -0,0 +1,77 @@
+// Copyright (c) ppy Pty Ltd