1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-07 15:32:58 +08:00

Merge branch 'master' into slider-start-circle

This commit is contained in:
Dan Balasescu 2020-03-30 14:49:17 +09:00 committed by GitHub
commit a0b8243f4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 437 additions and 110 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -30,6 +31,22 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5, Value = 5,
}; };
public override string SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty) protected override void TransferSettings(BeatmapDifficulty difficulty)
{ {
base.TransferSettings(difficulty); base.TransferSettings(difficulty);

View File

@ -0,0 +1,70 @@
// 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.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneSpinnerSpunOut : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SpinnerDisc),
typeof(DrawableSpinner),
typeof(DrawableOsuHitObject),
typeof(OsuModSpunOut)
};
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = new[] { new OsuModSpunOut() };
});
[Test]
public void TestSpunOut()
{
DrawableSpinner spinner = null;
AddStep("create spinner", () => spinner = createSpinner());
AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd);
AddAssert("spinner is completed", () => spinner.Progress >= 1);
}
private DrawableSpinner createSpinner()
{
var spinner = new Spinner
{
StartTime = Time.Current + 500,
EndTime = Time.Current + 2500
};
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableSpinner = new DrawableSpinner(spinner)
{
Anchor = Anchor.Centre
};
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
Add(drawableSpinner);
return drawableSpinner;
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -30,6 +31,22 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5, Value = 5,
}; };
public override string SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty) protected override void TransferSettings(BeatmapDifficulty difficulty)
{ {
base.TransferSettings(difficulty); base.TransferSettings(difficulty);

View File

@ -2,21 +2,46 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModSpunOut : Mod public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects
{ {
public override string Name => "Spun Out"; public override string Name => "Spun Out";
public override string Acronym => "SO"; public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunout; public override IconUsage? Icon => OsuIcon.ModSpunout;
public override ModType Type => ModType.DifficultyReduction; public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed."; public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9; public override double ScoreMultiplier => 0.9;
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var hitObject in drawables)
{
if (hitObject is DrawableSpinner spinner)
{
spinner.HandleUserInput = false;
spinner.OnUpdate += onSpinnerUpdate;
}
}
}
private void onSpinnerUpdate(Drawable drawable)
{
var spinner = (DrawableSpinner)drawable;
spinner.Disc.Tracking = true;
spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
}
} }
} }

View File

@ -176,17 +176,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update() protected override void Update()
{ {
Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
if (!SpmCounter.IsPresent && Disc.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
base.Update(); base.Update();
if (HandleUserInput)
Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
if (!SpmCounter.IsPresent && Disc.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
circle.Rotation = Disc.Rotation; circle.Rotation = Disc.Rotation;
Ticks.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation;
SpmCounter.SetRotation(Disc.RotationAbsolute); SpmCounter.SetRotation(Disc.RotationAbsolute);

View File

@ -73,6 +73,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
} }
} }
/// <summary>
/// Whether currently in the correct time range to allow spinning.
/// </summary>
private bool isSpinnableTime => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current;
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition); mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition);
@ -93,27 +98,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2));
bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; var delta = thisAngle - lastAngle;
if (validAndTracking) if (tracking)
{ Rotate(delta);
if (!rotationTransferred)
{
currentRotation = Rotation * 2;
rotationTransferred = true;
}
if (thisAngle - lastAngle > 180)
lastAngle += 360;
else if (lastAngle - thisAngle > 180)
lastAngle -= 360;
currentRotation += thisAngle - lastAngle;
RotationAbsolute += Math.Abs(thisAngle - lastAngle) * Math.Sign(Clock.ElapsedFrameTime);
}
lastAngle = thisAngle; lastAngle = thisAngle;
@ -128,5 +118,38 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
} }
/// <summary>
/// Rotate the disc by the provided angle (in addition to any existing rotation).
/// </summary>
/// <remarks>
/// Will be a no-op if not a valid time to spin.
/// </remarks>
/// <param name="angle">The delta angle.</param>
public void Rotate(float angle)
{
if (!isSpinnableTime)
return;
if (!rotationTransferred)
{
currentRotation = Rotation * 2;
rotationTransferred = true;
}
if (angle > 180)
{
lastAngle += 360;
angle -= 360;
}
else if (-angle > 180)
{
lastAngle -= 360;
angle += 360;
}
currentRotation += angle;
RotationAbsolute += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
}
} }
} }

View File

@ -113,7 +113,6 @@ namespace osu.Game.Rulesets.Osu
new OsuModEasy(), new OsuModEasy(),
new OsuModNoFail(), new OsuModNoFail(),
new MultiMod(new OsuModHalfTime(), new OsuModDaycore()), new MultiMod(new OsuModHalfTime(), new OsuModDaycore()),
new OsuModSpunOut(),
}; };
case ModType.DifficultyIncrease: case ModType.DifficultyIncrease:
@ -139,6 +138,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModAutoplay(), new OsuModCinema()), new MultiMod(new OsuModAutoplay(), new OsuModCinema()),
new OsuModRelax(), new OsuModRelax(),
new OsuModAutopilot(), new OsuModAutopilot(),
new OsuModSpunOut(),
}; };
case ModType.Fun: case ModType.Fun:

View File

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
@ -74,9 +73,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Beatmap.Value = working; Beatmap.Value = working;
SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
Player?.Exit();
Player = null;
Player = CreatePlayer(ruleset); Player = CreatePlayer(ruleset);
LoadScreen(Player); LoadScreen(Player);

View File

@ -4,9 +4,13 @@
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -17,21 +21,38 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
return new TestPlayer(false, false); return new TestPlayer(false);
} }
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.ChildrenOfType<BreakTracker>().First().Breaks.First().StartTime)); seekToBreak(0);
AddUntilStep("wait for seek to complete", () =>
Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime);
AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
seekToBreak(0);
seekToBreak(1);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => getResultsScreen() != null);
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen;
}
private void seekToBreak(int breakIndex)
{
AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime));
AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= destBreak().StartTime);
BreakPeriod destBreak() => Player.ChildrenOfType<BreakTracker>().First().Breaks.ElementAt(breakIndex);
} }
} }
} }

View File

@ -17,13 +17,12 @@ using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
using osu.Game.Tests.Visual.UserInterface; using osu.Game.Tests.Visual.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneReplayRecorder : OsuManualInputManagerTestScene public class TestSceneReplayRecorder : OsuManualInputManagerTestScene
{ {

View File

@ -13,12 +13,11 @@ using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
using osu.Game.Tests.Visual.UserInterface; using osu.Game.Tests.Visual.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneReplayRecording : OsuTestScene public class TestSceneReplayRecording : OsuTestScene
{ {

View File

@ -83,6 +83,82 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForSelection(set_count, 3); waitForSelection(set_count, 3);
} }
[TestCase(true)]
[TestCase(false)]
public void TestTraversalBeyondVisible(bool forwards)
{
var sets = new List<BeatmapSetInfo>();
const int total_set_count = 200;
for (int i = 0; i < total_set_count; i++)
sets.Add(createTestBeatmapSet(i + 1));
loadBeatmaps(sets);
for (int i = 1; i < total_set_count; i += i)
selectNextAndAssert(i);
void selectNextAndAssert(int amount)
{
setSelected(forwards ? 1 : total_set_count, 1);
AddStep($"{(forwards ? "Next" : "Previous")} beatmap {amount} times", () =>
{
for (int i = 0; i < amount; i++)
{
carousel.SelectNext(forwards ? 1 : -1);
}
});
waitForSelection(forwards ? amount + 1 : total_set_count - amount);
}
}
[Test]
public void TestTraversalBeyondVisibleDifficulties()
{
var sets = new List<BeatmapSetInfo>();
const int total_set_count = 20;
for (int i = 0; i < total_set_count; i++)
sets.Add(createTestBeatmapSet(i + 1));
loadBeatmaps(sets);
// Selects next set once, difficulty index doesn't change
selectNextAndAssert(3, true, 2, 1);
// Selects next set 16 times (50 \ 3 == 16), difficulty index changes twice (50 % 3 == 2)
selectNextAndAssert(50, true, 17, 3);
// Travels around the carousel thrice (200 \ 60 == 3)
// continues to select 20 times (200 \ 60 == 20)
// selects next set 6 times (20 \ 3 == 6)
// difficulty index changes twice (20 % 3 == 2)
selectNextAndAssert(200, true, 7, 3);
// All same but in reverse
selectNextAndAssert(3, false, 19, 3);
selectNextAndAssert(50, false, 4, 1);
selectNextAndAssert(200, false, 14, 1);
void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff)
{
// Select very first or very last difficulty
setSelected(forwards ? 1 : 20, forwards ? 1 : 3);
AddStep($"{(forwards ? "Next" : "Previous")} difficulty {amount} times", () =>
{
for (int i = 0; i < amount; i++)
carousel.SelectNext(forwards ? 1 : -1, false);
});
waitForSelection(expectedSet, expectedDiff);
}
}
/// <summary> /// <summary>
/// Test filtering /// Test filtering
/// </summary> /// </summary>

View File

@ -91,13 +91,14 @@ namespace osu.Game.Tests.Visual.UserInterface
var easierMods = osu.GetModsFor(ModType.DifficultyReduction); var easierMods = osu.GetModsFor(ModType.DifficultyReduction);
var harderMods = osu.GetModsFor(ModType.DifficultyIncrease); var harderMods = osu.GetModsFor(ModType.DifficultyIncrease);
var conversionMods = osu.GetModsFor(ModType.Conversion);
var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden);
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
var spunOutMod = easierMods.FirstOrDefault(m => m is OsuModSpunOut); var targetMod = conversionMods.FirstOrDefault(m => m is OsuModTarget);
var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy);
var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock);
@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface
testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour); testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour);
testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour); testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour);
testUnimplementedMod(spunOutMod); testUnimplementedMod(targetMod);
} }
[Test] [Test]

View File

@ -2,9 +2,15 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.IO.Serialization; using osu.Game.IO.Serialization;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -42,6 +48,51 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual string Description => string.Empty; public virtual string Description => string.Empty;
/// <summary>
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.
/// </summary>
/// <remarks>
/// Differs from <see cref="Name"/>, as the value of attributes (AR, CS, etc) changeable via the mod
/// are displayed in the tooltip.
/// </remarks>
[JsonIgnore]
public string IconTooltip
{
get
{
string description = SettingDescription;
return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})";
}
}
/// <summary>
/// The description of editable settings of a mod to use in the <see cref="IconTooltip"/>.
/// </summary>
/// <remarks>
/// Parentheses are added to the tooltip, surrounding the value of this property. If this property is <c>string.Empty</c>,
/// the tooltip will not have parentheses.
/// </remarks>
public virtual string SettingDescription
{
get
{
var tooltipTexts = new List<string>();
foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties())
{
object bindableObj = property.GetValue(this);
if ((bindableObj as IHasDefaultValue)?.IsDefault == true)
continue;
tooltipTexts.Add($"{attr.Label} {bindableObj}");
}
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));
}
}
/// <summary> /// <summary>
/// The score multiplier of this mod. /// The score multiplier of this mod.
/// </summary> /// </summary>

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Configuration; using osu.Game.Configuration;
using System.Linq;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -52,6 +53,21 @@ namespace osu.Game.Rulesets.Mods
Value = 5, Value = 5,
}; };
public override string SettingDescription
{
get
{
string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:N1}";
string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:N1}";
return string.Join(", ", new[]
{
drainRate,
overallDifficulty
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
private BeatmapDifficulty difficulty; private BeatmapDifficulty difficulty;
public void ReadFromDifficulty(BeatmapDifficulty difficulty) public void ReadFromDifficulty(BeatmapDifficulty difficulty)
@ -79,7 +95,7 @@ namespace osu.Game.Rulesets.Mods
/// <summary> /// <summary>
/// Transfer a setting from <see cref="BeatmapDifficulty"/> to a configuration bindable. /// Transfer a setting from <see cref="BeatmapDifficulty"/> to a configuration bindable.
/// Only performs the transfer if the user it not currently overriding.. /// Only performs the transfer if the user is not currently overriding.
/// </summary> /// </summary>
protected void TransferSetting<T>(BindableNumber<T> bindable, T beatmapDefault) protected void TransferSetting<T>(BindableNumber<T> bindable, T beatmapDefault)
where T : struct, IComparable<T>, IConvertible, IEquatable<T> where T : struct, IComparable<T>, IConvertible, IEquatable<T>

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
MaxValue = 10 MaxValue = 10
}; };
public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}";
private int retries; private int retries;
private BindableNumber<double> health; private BindableNumber<double> health;

View File

@ -15,5 +15,7 @@ namespace osu.Game.Rulesets.Mods
{ {
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
} }
} }

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mods
[SettingSource("Final rate", "The final speed to ramp to")] [SettingSource("Final rate", "The final speed to ramp to")]
public abstract BindableNumber<double> FinalRate { get; } public abstract BindableNumber<double> FinalRate { get; }
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";
private double finalRateTime; private double finalRateTime;
private double beginRampTime; private double beginRampTime;

View File

@ -38,6 +38,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>(); private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>(); public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>();
/// <summary>
/// Whether this object should handle any user input events.
/// </summary>
public bool HandleUserInput { get; set; } = true;
public override bool PropagatePositionalInputSubTree => HandleUserInput;
public override bool PropagateNonPositionalInputSubTree => HandleUserInput;
/// <summary> /// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>. /// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI
private readonly ModType type; private readonly ModType type;
public virtual string TooltipText { get; } public virtual string TooltipText => mod.IconTooltip;
private Mod mod; private Mod mod;
@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.UI
type = mod.Type; type = mod.Type;
TooltipText = mod.Name;
Size = new Vector2(size); Size = new Vector2(size);
Children = new Drawable[] Children = new Drawable[]

View File

@ -637,6 +637,39 @@ namespace osu.Game.Screens.Play
return base.OnExiting(next); return base.OnExiting(next);
} }
protected virtual void GotoRanking()
{
if (DrawableRuleset.ReplayScore != null)
{
// if a replay is present, we likely don't want to import into the local database.
this.Push(CreateResults(CreateScore()));
return;
}
LegacyByteArrayReader replayReader = null;
var score = new Score { ScoreInfo = CreateScore() };
if (recordingReplay?.Frames.Count > 0)
{
score.Replay = recordingReplay;
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
}
scoreManager.Import(score.ScoreInfo, replayReader)
.ContinueWith(imported => Schedule(() =>
{
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(imported.Result));
}));
}
private void fadeOut(bool instant = false) private void fadeOut(bool instant = false)
{ {
float fadeOutDuration = instant ? 0 : 250; float fadeOutDuration = instant ? 0 : 250;
@ -649,36 +682,7 @@ namespace osu.Game.Screens.Play
private void scheduleGotoRanking() private void scheduleGotoRanking()
{ {
completionProgressDelegate?.Cancel(); completionProgressDelegate?.Cancel();
completionProgressDelegate = Schedule(delegate completionProgressDelegate = Schedule(GotoRanking);
{
if (DrawableRuleset.ReplayScore != null)
this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo));
else
{
var score = new Score { ScoreInfo = CreateScore() };
LegacyByteArrayReader replayReader = null;
if (recordingReplay?.Frames.Count > 0)
{
score.Replay = recordingReplay;
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
}
scoreManager.Import(score.ScoreInfo, replayReader)
.ContinueWith(imported => Schedule(() =>
{
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(imported.Result));
}));
}
});
} }
#endregion #endregion

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Screens;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
@ -23,6 +24,11 @@ namespace osu.Game.Screens.Play
DrawableRuleset?.SetReplayScore(score); DrawableRuleset?.SetReplayScore(score);
} }
protected override void GotoRanking()
{
this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo));
}
protected override ScoreInfo CreateScore() => score.ScoreInfo; protected override ScoreInfo CreateScore() => score.ScoreInfo;
} }
} }

View File

@ -31,13 +31,13 @@ namespace osu.Game.Screens.Ranking
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Player player { get; set; } private Player player { get; set; }
private readonly ScoreInfo score; public readonly ScoreInfo Score;
private Drawable bottomPanel; private Drawable bottomPanel;
public ResultsScreen(ScoreInfo score) public ResultsScreen(ScoreInfo score)
{ {
this.score = score; Score = score;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking
{ {
new ResultsScrollContainer new ResultsScrollContainer
{ {
Child = new ScorePanel(score) Child = new ScorePanel(Score)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -77,7 +77,7 @@ namespace osu.Game.Screens.Ranking
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
new ReplayDownloadButton(score) { Width = 300 }, new ReplayDownloadButton(Score) { Width = 300 },
new RetryButton { Width = 300 }, new RetryButton { Width = 300 },
} }
} }

View File

@ -253,46 +253,37 @@ namespace osu.Game.Screens.Select
/// <param name="skipDifficulties">Whether to skip individual difficulties and only increment over full groups.</param> /// <param name="skipDifficulties">Whether to skip individual difficulties and only increment over full groups.</param>
public void SelectNext(int direction = 1, bool skipDifficulties = true) public void SelectNext(int direction = 1, bool skipDifficulties = true)
{ {
var visibleItems = Items.Where(s => !s.Item.Filtered.Value).ToList(); if (beatmapSets.All(s => s.Filtered.Value))
if (!visibleItems.Any())
return; return;
DrawableCarouselItem drawable = null; if (skipDifficulties)
selectNextSet(direction, true);
else
selectNextDifficulty(direction);
}
if (selectedBeatmap != null && (drawable = selectedBeatmap.Drawables.FirstOrDefault()) == null) private void selectNextSet(int direction, bool skipDifficulties)
// if the selected beatmap isn't present yet, we can't correctly change selection. {
// we can fix this by changing this method to not reference drawables / Items in the first place. var unfilteredSets = beatmapSets.Where(s => !s.Filtered.Value).ToList();
return;
int originalIndex = visibleItems.IndexOf(drawable); var nextSet = unfilteredSets[(unfilteredSets.IndexOf(selectedBeatmapSet) + direction + unfilteredSets.Count) % unfilteredSets.Count];
int currentIndex = originalIndex;
// local function to increment the index in the required direction, wrapping over extremities. if (skipDifficulties)
int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count; select(nextSet);
else
select(direction > 0 ? nextSet.Beatmaps.First(b => !b.Filtered.Value) : nextSet.Beatmaps.Last(b => !b.Filtered.Value));
}
while (incrementIndex() != originalIndex) private void selectNextDifficulty(int direction)
{ {
var item = visibleItems[currentIndex].Item; var unfilteredDifficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList();
if (item.Filtered.Value || item.State.Value == CarouselItemState.Selected) continue; int index = unfilteredDifficulties.IndexOf(selectedBeatmap);
switch (item) if (index + direction < 0 || index + direction >= unfilteredDifficulties.Count)
{ selectNextSet(direction, false);
case CarouselBeatmap beatmap: else
if (skipDifficulties) continue; select(unfilteredDifficulties[index + direction]);
select(beatmap);
return;
case CarouselBeatmapSet set:
if (skipDifficulties)
select(set);
else
select(direction > 0 ? set.Beatmaps.First(b => !b.Filtered.Value) : set.Beatmaps.Last(b => !b.Filtered.Value));
return;
}
}
} }
/// <summary> /// <summary>