mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 13:15:08 +08:00
Merge branch 'master' into localisation-settings
This commit is contained in:
commit
e9ab465da6
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
4
.github/workflows/report-nunit.yml
vendored
4
.github/workflows/report-nunit.yml
vendored
@ -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' }}
|
||||
|
3
.github/workflows/sentry-release.yml
vendored
3
.github/workflows/sentry-release.yml
vendored
@ -5,6 +5,9 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
sentry_release:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -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. -->
|
||||
|
@ -137,12 +137,13 @@ namespace osu.Desktop
|
||||
{
|
||||
base.SetHost(host);
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
|
||||
var desktopWindow = (SDL2DesktopWindow)host.Window;
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
|
||||
desktopWindow.CursorState |= CursorState.Hidden;
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
desktopWindow.Title = Name;
|
||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
});
|
||||
}
|
||||
|
||||
private class TestSkin : DefaultSkin
|
||||
private class TestSkin : TrianglesSkin
|
||||
{
|
||||
public bool FlipCatcherPlate { get; set; }
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
|
||||
|
@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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()
|
||||
|
102
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
Normal file
102
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
149
osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
Normal file
149
osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
@ -47,12 +48,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
private ShakeContainer shakeContainer;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
scaleContainer = new Container
|
||||
{
|
||||
@ -72,22 +75,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return true;
|
||||
},
|
||||
},
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
|
||||
shakeContainer = new ShakeContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
ShakeDuration = 30,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(4),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(4),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Size = HitArea.DrawSize;
|
||||
|
||||
@ -123,6 +134,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shake() => shakeContainer.Shake();
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
@ -139,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
|
||||
{
|
||||
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
|
||||
Shake();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -191,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// todo: temporary / arbitrary, used for lifetime optimisation.
|
||||
this.Delay(800).FadeOut();
|
||||
|
||||
// in the case of an early state change, the fade should be expedited to the current point in time.
|
||||
if (HitStateUpdateTime < HitObject.StartTime)
|
||||
ApproachCircle.FadeOut(50);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
default:
|
||||
ApproachCircle.FadeOut();
|
||||
break;
|
||||
|
||||
case ArmedState.Idle:
|
||||
HitArea.HitAction = null;
|
||||
break;
|
||||
|
@ -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"/>.
|
||||
|
@ -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[]
|
||||
{
|
||||
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 },
|
||||
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();
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
80
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs
Normal file
80
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs
Normal 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),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
29
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
Normal file
29
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
71
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
Normal file
71
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
39
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
Normal file
39
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
171
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
Normal file
171
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
226
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
Normal file
226
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
Normal file
54
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
109
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
Normal file
109
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
40
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
Normal file
40
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
146
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
Normal file
146
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
247
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
Normal file
247
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
Normal file
61
osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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++)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
117
osu.Game.Tests/Visual/Gameplay/TestSceneColourHitErrorMeter.cs
Normal file
117
osu.Game.Tests/Visual/Gameplay/TestSceneColourHitErrorMeter.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user