1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:35:10 +08:00

Merge branch 'master' into localisation-settings

This commit is contained in:
Dean Herbert 2022-10-06 13:22:08 +09:00 committed by GitHub
commit e9ab465da6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
199 changed files with 3919 additions and 943 deletions

View File

@ -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

View File

@ -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' }}

View File

@ -5,6 +5,9 @@ on:
tags:
- '*'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
sentry_release:
runs-on: ubuntu-latest

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.916.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1005.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -137,12 +137,13 @@ namespace osu.Desktop
{
base.SetHost(host);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
var desktopWindow = (SDL2DesktopWindow)host.Window;
desktopWindow.CursorState |= CursorState.Hidden;
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
}

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
});
}
private class TestSkin : DefaultSkin
private class TestSkin : TrianglesSkin
{
public bool FlipCatcherPlate { get; set; }

View File

@ -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<HitExplosion>().First()?.Entry?.ObjectColour == fruitColour);
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
}
[Test]

View File

@ -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,22 +15,14 @@ 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;

View File

@ -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<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};

View File

@ -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 <see cref="X"/> value plus the offset applied by the beatmap processing.
/// Use <see cref="OriginalX"/> if a value not affected by the offset is desired.
/// </remarks>
public float EffectiveX => OriginalX + XOffset;
public float EffectiveX => Math.Clamp(OriginalX + XOffset, 0, CatchPlayfield.WIDTH);
public double TimePreempt { get; set; } = 1000;

View File

@ -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

View File

@ -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");

View File

@ -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]

View File

@ -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;

View File

@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
protected override void UpdateInitialTransforms()
{
}
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
}
}

View File

@ -30,14 +30,6 @@ 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.

View File

@ -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)
{

View File

@ -23,10 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
// 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; }

View File

@ -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<ShakeContainer>().First().Children.OfType<Container>().Single().Scale.X, target));
return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType<Container>().First().Scale.X, target));
}
private bool checkSomeHit()

View File

@ -0,0 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using 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<int> 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;
}
}
}

View File

@ -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;
});
});
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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<SpriteIcon>().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

View File

@ -0,0 +1,149 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.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<SpriteIcon>().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<OsuReplayFrame>()
.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<ReplayFrame>()
.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<Replay, Replay> replayTransformation) => AddStep("set replay", () =>
{
var drawableRuleset = this.ChildrenOfType<DrawableOsuRuleset>().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<HitObject>
{
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)
{
}
}
}
}

View File

@ -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;

View File

@ -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(),

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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)
{

View File

@ -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)

View File

@ -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<float> StartScale { get; } = new BindableFloat
public override BindableNumber<float> StartScale { get; } = new BindableFloat(2)
{
MinValue = 1f,
MaxValue = 25f,
Default = 2f,
Value = 2f,
Precision = 0.1f,
};
}

View File

@ -32,22 +32,14 @@ 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;

View File

@ -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<float> StartScale { get; } = new BindableFloat
public override BindableNumber<float> StartScale { get; } = new BindableFloat(0.5f)
{
MinValue = 0f,
MaxValue = 0.99f,
Default = 0.5f,
Value = 0.5f,
Precision = 0.01f,
};
}

View File

@ -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<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};

View File

@ -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<float> StartScale { get; }
protected virtual float EndScale => 1;

View File

@ -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<float>))]
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);
}
/// <param name="targetDistance">The target distance between the previous and the current <see cref="OsuHitObject"/>.</param>
/// <param name="offset">The angle (in rad) by which the target angle should be offset.</param>
/// <param name="flowDirection">Whether the relative angle should be positive or negative.</param>
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;
}

View File

@ -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<int?> Seed { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
public Bindable<int?> Seed { get; } = new Bindable<int?>();
[SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")]
public Bindable<bool> Metronome { get; } = new BindableBool(true);

View File

@ -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,6 +75,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
{
Anchor = Anchor.Centre,
@ -86,8 +95,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
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;

View File

@ -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<OsuHitObject>
public abstract class DrawableOsuHitObject : DrawableHitObject<OsuHitObject>
{
public readonly IBindable<Vector2> PositionBindable = new Bindable<Vector2>();
public readonly IBindable<int> StackHeightBindable = new Bindable<int>();
@ -34,8 +35,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public Func<DrawableHitObject, double, bool> 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);
/// <summary>
/// Shake the hit object in case it was clicked far too early or late (aka "note lock").
/// </summary>
public virtual void Shake() { }
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.

View File

@ -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;
/// <summary>
/// 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[]
{
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<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
}
},
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
headContainer = new Container<DrawableSliderHead> { 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();

View File

@ -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<double> OnShake;
public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
public override void Shake()
{
base.Shake();
DrawableSlider.Shake();
}
private void updatePosition()
{

View File

@ -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));
}

View File

@ -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));
}

View File

@ -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));
}

View File

@ -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;
/// <summary>
/// Apply a judgement result.

View File

@ -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;
}

View File

@ -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;
@ -237,6 +238,9 @@ namespace osu.Game.Rulesets.Osu
{
case LegacySkin:
return new OsuLegacySkinTransformer(skin);
case ArgonSkin:
return new OsuArgonSkinTransformer(skin);
}
return null;

View File

@ -1,20 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuHitWindows : HitWindows
{
/// <summary>
/// osu! ruleset has a fixed miss window regardless of difficulty settings.
/// </summary>
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)

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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),
},
},
};
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.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;
}
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.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()
{
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.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,
},
};
}
}
}

View File

@ -0,0 +1,171 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.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),
});
}
}
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>
/// <remarks>
/// The base implementation only handles fade (for all result types) and misses.
/// Individual rulesets are recommended to implement their appropriate hit animations.
/// </remarks>
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);
}
}
}
}

View File

@ -0,0 +1,226 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.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<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
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,
};
}
}
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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<Color4> 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);
}
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.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;
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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);
}
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.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<Color4> 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);
}
}
}

View File

@ -0,0 +1,146 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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<double> gainedBonus = null!;
private IBindable<double> 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;
}
}
}

View File

@ -0,0 +1,247 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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;
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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,
}
}
});
}
}
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Rulesets.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<HitResult> resultComponent:
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponent osuComponent:
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);
}
}
}

View File

@ -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;

View File

@ -16,9 +16,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class PlaySliderBody : SnakingSliderBody
{
private IBindable<float> scaleBindable;
protected IBindable<float> ScaleBindable { get; private set; } = null!;
protected IBindable<Color4> AccentColourBindable { get; private set; } = null!;
private IBindable<int> pathVersion;
private IBindable<Color4> 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);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#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

View File

@ -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
{

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
@ -129,5 +130,32 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult<Hit>(0, HitResult.Miss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
}
[Test]
public void TestHighVelocityHit()
{
const double hit_time = 1000;
var beatmap = CreateBeatmap(new Hit
{
Type = HitType.Centre,
StartTime = hit_time,
});
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 });
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 });
var hitWindows = new HitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time - hitWindows.WindowFor(HitResult.Great), TaikoAction.LeftCentre),
}, beatmap);
AssertJudgementCount(1);
AssertResult<Hit>(0, HitResult.Ok);
}
}
}

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countOk;
private int countMeh;
private int countMiss;
private double accuracy;
private double effectiveMissCount;
@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
accuracy = customAccuracy;
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0)
@ -87,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.050 * lengthBonus;
return difficultyValue * Math.Pow(score.Accuracy, 2.0);
return difficultyValue * Math.Pow(accuracy, 2.0);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0)
return 0;
double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;
@ -110,5 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
}
}

View File

@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Layout;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@ -17,22 +16,14 @@ namespace osu.Game.Rulesets.Taiko.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 => 250;

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Filled = HitObject.FirstTick
});
protected override double MaximumJudgementOffset => HitObject.HitWindow;
public override double MaximumJudgementOffset => HitObject.HitWindow;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{

View File

@ -306,7 +306,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(128, 255, 128, 255),
new Color4(255, 187, 255, 255),
new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 100),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++)

View File

@ -204,31 +204,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModDifficultyAdjust : ModDifficultyAdjust

View File

@ -124,31 +124,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModEnum : Mod

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -78,7 +77,7 @@ namespace osu.Game.Tests.Online
}
};
beatmaps.AllowImport = new TaskCompletionSource<bool>();
beatmaps.AllowImport.Reset();
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
@ -132,7 +131,7 @@ namespace osu.Game.Tests.Online
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsSoftDeleting()
{
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
@ -155,7 +154,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsChecksum()
{
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
@ -202,7 +201,7 @@ namespace osu.Game.Tests.Online
private class TestBeatmapManager : BeatmapManager
{
public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>();
public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
public Live<BeatmapSetInfo> CurrentImport { get; private set; }
@ -229,7 +228,9 @@ namespace osu.Game.Tests.Online
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
{
testBeatmapManager.AllowImport.Task.WaitSafely();
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
throw new TimeoutException("Timeout waiting for import to be allowed.");
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
}
}

View File

@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins.IO
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Skins.IO
{
Assert.IsFalse(s.Protected);
Assert.AreNotEqual(originalSkinId, s.ID);
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
});
return Task.CompletedTask;
@ -226,7 +226,7 @@ namespace osu.Game.Tests.Skins.IO
{
var skinManager = osu.Dependencies.Get<SkinManager>();
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
skinManager.EnsureMutableSkin();

View File

@ -29,7 +29,7 @@ namespace osu.Game.Tests.Skins
new Color4(142, 199, 255, 255),
new Color4(255, 128, 128, 255),
new Color4(128, 255, 255, 255),
new Color4(100, 100, 100, 100),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
Assert.AreEqual(expectedColors.Count, comboColors.Count);

View File

@ -1,12 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -19,7 +18,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
SetContents(skin =>
{
var implementation = skin != null
var implementation = skin is LegacySkin
? CreateLegacyImplementation()
: CreateDefaultImplementation();

View File

@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneColourHitErrorMeter : OsuTestScene
{
private DependencyProvidingContainer dependencyContainer = null!;
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
private ScoreProcessor scoreProcessor = null!;
private int iteration;
private ColourHitErrorMeter colourHitErrorMeter = null!;
public TestSceneColourHitErrorMeter()
{
AddSliderStep("Judgement spacing", 0, 10, 2, spacing =>
{
if (colourHitErrorMeter.IsNotNull())
colourHitErrorMeter.JudgementSpacing.Value = spacing;
});
AddSliderStep("Judgement count", 1, 50, 5, spacing =>
{
if (colourHitErrorMeter.IsNotNull())
colourHitErrorMeter.JudgementCount.Value = spacing;
});
}
[SetUpSteps]
public void SetupSteps() => AddStep("Create components", () =>
{
var ruleset = CreateRuleset();
Debug.Assert(ruleset != null);
scoreProcessor = new ScoreProcessor(ruleset);
Child = dependencyContainer = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ScoreProcessor), scoreProcessor)
}
};
dependencyContainer.Child = colourHitErrorMeter = new ColourHitErrorMeter
{
Margin = new MarginPadding
{
Top = 100
},
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Scale = new Vector2(2),
};
});
protected override Ruleset CreateRuleset() => new OsuRuleset();
[Test]
public void TestSpacingChange()
{
AddRepeatStep("Add judgement", applyOneJudgement, 5);
AddStep("Change spacing", () => colourHitErrorMeter.JudgementSpacing.Value = 10);
AddRepeatStep("Add judgement", applyOneJudgement, 5);
}
[Test]
public void TestJudgementAmountChange()
{
AddRepeatStep("Add judgement", applyOneJudgement, 10);
AddStep("Judgement count change to 4", () => colourHitErrorMeter.JudgementCount.Value = 4);
AddRepeatStep("Add judgement", applyOneJudgement, 8);
}
[Test]
public void TestHitErrorShapeChange()
{
AddRepeatStep("Add judgement", applyOneJudgement, 8);
AddStep("Change shape square", () => colourHitErrorMeter.JudgementShape.Value = ColourHitErrorMeter.ShapeStyle.Square);
AddRepeatStep("Add judgement", applyOneJudgement, 10);
AddStep("Change shape circle", () => colourHitErrorMeter.JudgementShape.Value = ColourHitErrorMeter.ShapeStyle.Circle);
}
private void applyOneJudgement()
{
lastJudgementResult.Value = new OsuJudgementResult(new HitObject
{
StartTime = iteration * 10000,
}, new OsuJudgement())
{
Type = HitResult.Great,
};
scoreProcessor.ApplyResult(lastJudgementResult.Value);
iteration++;
}
}
}

View File

@ -6,7 +6,9 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Extensions.PolygonExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
@ -18,37 +20,62 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public class TestSceneGameplayLeaderboard : OsuTestScene
{
private readonly TestGameplayLeaderboard leaderboard;
private TestGameplayLeaderboard leaderboard;
private readonly BindableDouble playerScore = new BindableDouble();
public TestSceneGameplayLeaderboard()
{
Add(leaderboard = new TestGameplayLeaderboard
AddStep("toggle expanded", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2),
if (leaderboard != null)
leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
});
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
}
[SetUpSteps]
public void SetUpSteps()
[Test]
public void TestLayoutWithManyScores()
{
AddStep("reset leaderboard", () =>
createLeaderboard();
AddStep("add many scores in one go", () =>
{
leaderboard.Clear();
playerScore.Value = 1222333;
for (int i = 0; i < 32; i++)
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
// Add player at end to force an animation down the whole list.
playerScore.Value = 0;
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
});
AddStep("add local player", () => createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true));
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
// Gameplay leaderboard has custom scroll logic, which when coupled with LayoutDuration
// has caused layout to not work in the past.
AddUntilStep("wait for fill flow layout",
() => leaderboard.ChildrenOfType<FillFlowContainer<GameplayLeaderboardScore>>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
AddUntilStep("wait for some scores not masked away",
() => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
AddStep("change score to middle", () => playerScore.Value = 1000000);
AddWaitStep("wait for movement", 5);
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
AddStep("change score to first", () => playerScore.Value = 5000000);
AddWaitStep("wait for movement", 5);
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
}
[Test]
public void TestPlayerScore()
{
createLeaderboard();
addLocalPlayer();
var player2Score = new BindableDouble(1234567);
var player3Score = new BindableDouble(1111111);
@ -73,6 +100,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRandomScores()
{
createLeaderboard();
addLocalPlayer();
int playerNumber = 1;
AddRepeatStep("add player with random score", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10);
}
@ -80,6 +110,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestExistingUsers()
{
createLeaderboard();
addLocalPlayer();
AddStep("add peppy", () => createRandomScore(new APIUser { Username = "peppy", Id = 2 }));
AddStep("add smoogipoo", () => createRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
AddStep("add flyte", () => createRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
@ -89,6 +122,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMaxHeight()
{
createLeaderboard();
addLocalPlayer();
int playerNumber = 1;
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
checkHeight(4);
@ -103,6 +139,28 @@ namespace osu.Game.Tests.Visual.Gameplay
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
}
private void addLocalPlayer()
{
AddStep("add local player", () =>
{
playerScore.Value = 1222333;
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
});
}
private void createLeaderboard()
{
AddStep("create leaderboard", () =>
{
Child = leaderboard = new TestGameplayLeaderboard
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2),
};
});
}
private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
private void createLeaderboardScore(BindableDouble score, APIUser user, bool isTracked = false)

View File

@ -107,13 +107,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("circle added", () =>
this.ChildrenOfType<ColourHitErrorMeter>().All(
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 1));
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Count() == 1));
AddStep("miss", () => newJudgement(50, HitResult.Miss));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("circle added", () =>
this.ChildrenOfType<ColourHitErrorMeter>().All(
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 2));
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Count() == 2));
}
[Test]
@ -123,11 +123,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("small bonus", () => newJudgement(result: HitResult.SmallBonus));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Any());
AddStep("large bonus", () => newJudgement(result: HitResult.LargeBonus));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Any());
}
[Test]
@ -137,16 +137,17 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("ignore hit", () => newJudgement(result: HitResult.IgnoreHit));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Any());
AddStep("ignore miss", () => newJudgement(result: HitResult.IgnoreMiss));
AddAssert("no bars added", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Any());
}
[Test]
public void TestProcessingWhileHidden()
{
const int max_displayed_judgements = 20;
AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
AddStep("hide displays", () =>
@ -155,16 +156,16 @@ namespace osu.Game.Tests.Visual.Gameplay
hitErrorMeter.Hide();
});
AddRepeatStep("hit", () => newJudgement(), ColourHitErrorMeter.MAX_DISPLAYED_JUDGEMENTS * 2);
AddRepeatStep("hit", () => newJudgement(), max_displayed_judgements * 2);
AddAssert("bars added", () => this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("circle added", () => this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddAssert("circle added", () => this.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Any());
AddUntilStep("wait for bars to disappear", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddUntilStep("ensure max circles not exceeded", () =>
{
return this.ChildrenOfType<ColourHitErrorMeter>()
.All(m => m.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() <= ColourHitErrorMeter.MAX_DISPLAYED_JUDGEMENTS);
.All(m => m.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Count() <= max_displayed_judgements);
});
AddStep("show displays", () =>
@ -183,12 +184,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("bar added", () => this.ChildrenOfType<BarHitErrorMeter>().All(
meter => meter.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Count() == 1));
AddAssert("circle added", () => this.ChildrenOfType<ColourHitErrorMeter>().All(
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 1));
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Count() == 1));
AddStep("clear", () => this.ChildrenOfType<HitErrorMeter>().ForEach(meter => meter.Clear()));
AddAssert("bar cleared", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
AddAssert("colour cleared", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
AddAssert("colour cleared", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorShape>().Any());
}
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)

View File

@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
private TestParticleSpewer createSpewer() =>
new TestParticleSpewer(skinManager.DefaultLegacySkin.GetTexture("star2"))
new TestParticleSpewer(skinManager.DefaultClassicSkin.GetTexture("star2"))
{
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,

View File

@ -264,13 +264,13 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMutedNotificationMasterVolume()
{
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.IsDefault);
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.Value == 0.5);
}
[Test]
public void TestMutedNotificationTrackVolume()
{
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.IsDefault);
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.Value == 0.5);
}
[Test]

View File

@ -15,8 +15,10 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@ -101,6 +103,37 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
}
[Test]
public void TestModReferenceNotRetained()
{
AddStep("allow fail", () => allowFail = false);
Mod[] originalMods = { new OsuModDaycore { SpeedChange = { Value = 0.8 } } };
Mod[] playerMods = null!;
AddStep("load player with mods", () => LoadPlayer(originalMods));
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
AddStep("get mods at start of gameplay", () => playerMods = Player.Score.ScoreInfo.Mods.ToArray());
// Player creates new instance of mods during load.
AddAssert("player score has copied mods", () => playerMods.First(), () => Is.Not.SameAs(originalMods.First()));
AddAssert("player score has matching mods", () => playerMods.First(), () => Is.EqualTo(originalMods.First()));
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
// Player creates new instance of mods after gameplay to ensure any runtime references to drawables etc. are not retained.
AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First()));
}
[Test]
public void TestScoreStoredLocally()
{

View File

@ -11,8 +11,10 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@ -167,14 +169,39 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
private void addHitObject(double time)
[Test]
public void TestVeryFlowScroll()
{
const double long_time_range = 100000;
var manualClock = new ManualClock();
AddStep("set manual clock", () =>
{
manualClock.CurrentTime = 0;
scrollContainers.ForEach(c => c.Clock = new FramedClock(manualClock));
setScrollAlgorithm(ScrollVisualisationMethod.Constant);
scrollContainers.ForEach(c => c.TimeRange = long_time_range);
});
AddStep("add hit objects", () =>
{
addHitObject(long_time_range);
addHitObject(long_time_range + 100, 250);
});
AddAssert("hit objects are alive", () => playfields.All(p => p.HitObjectContainer.AliveObjects.Count() == 2));
}
private void addHitObject(double time, float size = 75)
{
playfields.ForEach(p =>
{
var hitObject = new TestDrawableHitObject(time);
setAnchor(hitObject, p);
var hitObject = new TestHitObject(size) { StartTime = time };
var drawable = new TestDrawableHitObject(hitObject);
p.Add(hitObject);
setAnchor(drawable, p);
p.Add(drawable);
});
}
@ -248,6 +275,8 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
}
protected override ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new TestScrollingHitObjectContainer();
}
private class TestDrawableControlPoint : DrawableHitObject<HitObject>
@ -281,22 +310,41 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
private class TestDrawableHitObject : DrawableHitObject<HitObject>
private class TestHitObject : HitObject
{
public TestDrawableHitObject(double time)
: base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
{
Origin = Anchor.Custom;
OriginPosition = new Vector2(75 / 4.0f);
public readonly float Size;
AutoSizeAxes = Axes.Both;
public TestHitObject(float size)
{
Size = size;
}
}
private class TestDrawableHitObject : DrawableHitObject<TestHitObject>
{
public TestDrawableHitObject(TestHitObject hitObject)
: base(hitObject)
{
Origin = Anchor.Centre;
Size = new Vector2(hitObject.Size);
AddInternal(new Box
{
Size = new Vector2(75),
RelativeSizeAxes = Axes.Both,
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
});
}
}
private class TestScrollingHitObjectContainer : ScrollingHitObjectContainer
{
protected override RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry)
{
if (entry.HitObject is TestHitObject testObject)
return new RectangleF().Inflate(testObject.Size / 2);
return base.GetConservativeBoundingBox(entry);
}
}
}
}

View File

@ -0,0 +1,101 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSoloGameplayLeaderboard : OsuTestScene
{
[Cached]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
private SoloGameplayLeaderboard leaderboard = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear scores", () => scores.Clear());
AddStep("create component", () =>
{
var trackingUser = new APIUser
{
Username = "local user",
Id = 2,
};
Child = leaderboard = new SoloGameplayLeaderboard(trackingUser)
{
Scores = { BindTarget = scores },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AlwaysVisible = { Value = false },
Expanded = { Value = true },
};
});
AddStep("add scores", () => scores.AddRange(createSampleScores()));
}
[Test]
public void TestLocalUser()
{
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v);
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
}
[Test]
public void TestVisibility()
{
AddStep("set config visible true", () => configVisibility.Value = true);
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
AddStep("set config visible false", () => configVisibility.Value = false);
AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0);
AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true);
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
AddStep("set config visible true", () => configVisibility.Value = true);
AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1);
}
private static List<ScoreInfo> createSampleScores()
{
return new[]
{
new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
}.Concat(Enumerable.Range(0, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
}
}
}

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@ -51,13 +51,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestToggleSeeking()
{
DefaultSongProgress getDefaultProgress() => this.ChildrenOfType<DefaultSongProgress>().Single();
void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
AddStep("allow seeking", () => getDefaultProgress().AllowSeeking.Value = true);
AddStep("hide graph", () => getDefaultProgress().ShowGraph.Value = false);
AddStep("disallow seeking", () => getDefaultProgress().AllowSeeking.Value = false);
AddStep("allow seeking", () => getDefaultProgress().AllowSeeking.Value = true);
AddStep("show graph", () => getDefaultProgress().ShowGraph.Value = true);
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
AddStep("hide graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = false));
AddStep("disallow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = false));
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
AddStep("show graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = true));
}
private void setHitObjects()

View File

@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentBeatmapDuration;
private double currentStoryboardDuration;
private bool showResults = true;
@ -45,7 +46,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
AddStep("set beatmap duration to 0s", () => currentBeatmapDuration = 0);
AddStep("set storyboard duration to 8s", () => currentStoryboardDuration = 8000);
AddStep("set ShowResults = true", () => showResults = true);
}
@ -151,6 +153,24 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player exited", () => Stack.CurrentScreen == null);
}
[Test]
public void TestPerformExitAfterOutro()
{
CreateTest(() =>
{
AddStep("set beatmap duration to 4s", () => currentBeatmapDuration = 4000);
AddStep("set storyboard duration to 1s", () => currentStoryboardDuration = 1000);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("player paused", () => !Player.IsResuming);
AddStep("resume player", () => Player.Resume());
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@ -160,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle());
beatmap.HitObjects.Add(new HitCircle { StartTime = currentBeatmapDuration });
return beatmap;
}
@ -189,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
: base(false, showResults)
: base(showResults: showResults)
{
this.failConditions = failConditions;
}

View File

@ -120,6 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
private void assertCombo(int userId, int expectedCombo)
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
}
}

View File

@ -522,7 +522,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId);
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestEditDefaultSkin()
{
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.DEFAULT_SKIN);
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.ARGON_SKIN);
AddStep("open settings", () => { Game.Settings.Show(); });
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open skin editor", () => skinEditor.Show());
// Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part).
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.DEFAULT_SKIN);
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.ARGON_SKIN);
AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected));
AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType<SkinSection.ExportSkinButton>().SingleOrDefault()?.Enabled.Value == true);

View File

@ -9,8 +9,10 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
@ -92,6 +94,31 @@ namespace osu.Game.Tests.Visual.Navigation
returnToMenu();
}
[Test]
public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
AddUntilStep("wait for no results", () => Beatmap.IsDefault);
var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
presentAndConfirm(firstImport, type);
}
[Test]
public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
presentAndConfirm(firstImport, type);
}
[Test]
public void TestFromSongSelect([Values] ScorePresentType type)
{

View File

@ -29,11 +29,7 @@ namespace osu.Game.Tests.Visual.Settings
{
Child = textBox = new SettingsTextBox
{
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
Current = new Bindable<string>("test")
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
@ -59,11 +55,7 @@ namespace osu.Game.Tests.Visual.Settings
{
Child = textBox = new SettingsTextBox
{
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
Current = new Bindable<string>("test")
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);

View File

@ -67,11 +67,7 @@ namespace osu.Game.Tests.Visual.Settings
};
[SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>();
}
private enum TestEnum

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -36,10 +34,9 @@ namespace osu.Game.Tests.Visual.SongSelect
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay;
private ScoreManager scoreManager;
private RulesetStore rulesetStore;
private BeatmapManager beatmapManager;
private ScoreManager scoreManager = null!;
private RulesetStore rulesetStore = null!;
private BeatmapManager beatmapManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@ -74,7 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestLocalScoresDisplay()
{
BeatmapInfo beatmapInfo = null;
BeatmapInfo beatmapInfo = null!;
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
@ -387,7 +384,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private class FailableLeaderboard : BeatmapLeaderboard
{
public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state);
public new void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore);
public new void SetScores(IEnumerable<ScoreInfo>? scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore);
}
}
}

View File

@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("wait for fetch", () => leaderboard.Scores.Any());
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
// "Clean up"
@ -174,7 +174,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("wait for fetch", () => leaderboard.Scores.Any());
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID));
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -10,6 +11,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Updater;
@ -32,6 +34,8 @@ namespace osu.Game.Tests.Visual.UserInterface
[SetUp]
public void SetUp() => Schedule(() =>
{
InputManager.MoveMouseTo(Vector2.Zero);
TimeToCompleteProgress = 2000;
progressingNotifications.Clear();
@ -103,9 +107,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("start drag", () =>
{
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single());
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single());
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
});
AddStep("fling away", () =>
@ -119,6 +123,45 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("unread count zero", () => notificationOverlay.UnreadCount.Value == 0);
}
[Test]
public void TestProgressNotificationCantBeFlung()
{
bool activated = false;
ProgressNotification notification = null!;
AddStep("post", () =>
{
activated = false;
notificationOverlay.Post(notification = new ProgressNotification
{
Text = @"Uploading to BSS...",
CompletionText = "Uploaded to BSS!",
Activated = () => activated = true,
});
progressingNotifications.Add(notification);
});
AddStep("start drag", () =>
{
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single());
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
});
AddStep("attempt fling", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
});
AddUntilStep("was not closed", () => !notification.WasClosed);
AddUntilStep("was not cancelled", () => notification.State == ProgressNotificationState.Active);
AddAssert("was not activated", () => !activated);
AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
AddUntilStep("was completed", () => notification.State == ProgressNotificationState.Completed);
}
[Test]
public void TestDismissWithoutActivationCloseButton()
{
@ -228,6 +271,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait overlay not present", () => !notificationOverlay.IsPresent);
}
[Test]
public void TestProgressClick()
{
ProgressNotification notification = null!;
AddStep("add progress notification", () =>
{
notification = new ProgressNotification
{
Text = @"Uploading to BSS...",
CompletionText = "Uploaded to BSS!",
};
notificationOverlay.Post(notification);
progressingNotifications.Add(notification);
});
AddStep("hover over notification", () => InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<ProgressNotification>().Single()));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("not cancelled", () => notification.State == ProgressNotificationState.Active);
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("cancelled", () => notification.State == ProgressNotificationState.Cancelled);
}
[Test]
public void TestCompleteProgress()
{
@ -299,7 +367,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
SimpleNotification notification = null!;
AddStep(@"post", () => notificationOverlay.Post(notification = new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" }));
AddUntilStep("check is toast", () => !notification.IsInToastTray);
AddUntilStep("check is toast", () => notification.IsInToastTray);
AddAssert("light is not visible", () => notification.ChildrenOfType<Notification.NotificationLight>().Single().Alpha == 0);
AddUntilStep("wait for forward to overlay", () => !notification.IsInToastTray);
@ -424,11 +492,19 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRepeatStep("send barrage", sendBarrage, 10);
}
[Test]
public void TestServerShuttingDownNotification()
{
AddStep("post with 5 seconds", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromSeconds(5))));
AddStep("post with 30 seconds", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromSeconds(30))));
AddStep("post with 6 hours", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromHours(6))));
}
protected override void Update()
{
base.Update();
progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed);
progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed && n.WasClosed);
if (progressingNotifications.Count(n => n.State == ProgressNotificationState.Active) < 3)
{

View File

@ -24,7 +24,6 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
MinValue = 0.1,
MaxValue = 10
};

View File

@ -28,7 +28,6 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
MinValue = 0.01,
MaxValue = 10
};

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