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

Merge branch 'master' into move-setter-to-duration

This commit is contained in:
Dan Balasescu 2020-06-01 15:55:48 +09:00 committed by GitHub
commit 64d1b4b11b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1176 additions and 256 deletions

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj) switch (obj)
{ {
case IHasCurve curveData: case IHasPathWithRepeats curveData:
return new JuiceStream return new JuiceStream
{ {
StartTime = obj.StartTime, StartTime = obj.StartTime,

View File

@ -14,7 +14,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class JuiceStream : CatchHitObject, IHasCurve public class JuiceStream : CatchHitObject, IHasPathWithRepeats
{ {
/// <summary> /// <summary>
/// Positional distance that results in a duration of one second, before any speed adjustments. /// Positional distance that results in a duration of one second, before any speed adjustments.

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

View File

@ -474,7 +474,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <returns></returns> /// <returns></returns>
private IList<HitSampleInfo> sampleInfoListAt(double time) private IList<HitSampleInfo> sampleInfoListAt(double time)
{ {
if (!(HitObject is IHasCurve curveData)) if (!(HitObject is IHasPathWithRepeats curveData))
return HitObject.Samples; return HitObject.Samples;
double segmentTime = (EndTime - HitObject.StartTime) / spanCount; double segmentTime = (EndTime - HitObject.StartTime) / spanCount;

View File

@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
base.Update(); base.Update();
if (leftSprite?.Height > 0) if (leftSprite?.Height > 0)
leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height); leftSprite.Scale = new Vector2(1, DrawHeight / leftSprite.Height);
if (rightSprite?.Height > 0) if (rightSprite?.Height > 0)
rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height); rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height);
} }
} }
} }

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
switch (original) switch (original)
{ {
case IHasCurve curveData: case IHasPathWithRepeats curveData:
return new Slider return new Slider
{ {
StartTime = original.StartTime, StartTime = original.StartTime,

View File

@ -17,7 +17,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public class Slider : OsuHitObject, IHasCurve public class Slider : OsuHitObject, IHasPathWithRepeats
{ {
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring namespace osu.Game.Rulesets.Osu.Scoring
{ {
internal class OsuScoreProcessor : ScoreProcessor public class OsuScoreProcessor : ScoreProcessor
{ {
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement);

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{ {
List<IList<HitSampleInfo>> allSamples = obj is IHasCurve curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples }); List<IList<HitSampleInfo>> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List<IList<HitSampleInfo>>(new[] { samples });
int i = 0; int i = 0;

View File

@ -3,9 +3,7 @@
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -17,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
public class DrumRoll : TaikoHitObject, IHasCurve public class DrumRoll : TaikoHitObject, IHasPath
{ {
/// <summary> /// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length. /// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -115,11 +113,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity; double IHasDistance.Distance => Duration * Velocity;
int IHasRepeats.RepeatCount { get => 0; set { } } SliderPath IHasPath.Path
List<IList<HitSampleInfo>> IHasRepeats.NodeSamples => new List<IList<HitSampleInfo>>();
SliderPath IHasCurve.Path
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER); => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
#endregion #endregion

View File

@ -365,7 +365,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var hitObjects = decoder.Decode(stream).HitObjects; var hitObjects = decoder.Decode(stream).HitObjects;
var curveData = hitObjects[0] as IHasCurve; var curveData = hitObjects[0] as IHasPathWithRepeats;
var positionData = hitObjects[0] as IHasPosition; var positionData = hitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);

View File

@ -95,7 +95,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var beatmap = decodeAsJson(normal); var beatmap = decodeAsJson(normal);
var curveData = beatmap.HitObjects[0] as IHasCurve; var curveData = beatmap.HitObjects[0] as IHasPathWithRepeats;
var positionData = beatmap.HitObjects[0] as IHasPosition; var positionData = beatmap.HitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);

View File

@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable));
AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded));
createButton(false); createButton(false);
createButtonNoScore();
} }
private void createButton(bool withReplay) private void createButton(bool withReplay)
@ -40,6 +41,22 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.Centre, Origin = Anchor.Centre,
}; };
}); });
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
}
private void createButtonNoScore()
{
AddStep("create button with null score", () =>
{
Child = downloadButton = new TestReplayDownloadButton(null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
} }
private ScoreInfo getScoreInfo(bool replayAvailable) private ScoreInfo getScoreInfo(bool replayAvailable)

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Contracted;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneContractedPanelMiddleContent : OsuTestScene
{
[Resolved]
private RulesetStore rulesetStore { get; set; }
[Test]
public void TestShowPanel()
{
AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
}
private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score)
{
Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score);
}
private class ContractedPanelMiddleContentContainer : Container
{
[Cached]
private Bindable<WorkingBeatmap> workingBeatmap { get; set; }
public ContractedPanelMiddleContentContainer(WorkingBeatmap beatmap, ScoreInfo score)
{
workingBeatmap = new Bindable<WorkingBeatmap>(beatmap);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, 460);
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#353535"),
},
new ContractedPanelMiddleContent(score),
};
}
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -14,10 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Expanded; using osu.Game.Screens.Ranking.Expanded;
@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
var author = new User { Username = "mapper_name" }; var author = new User { Username = "mapper_name" };
AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore())); AddStep("show example score", () => showPanel(createTestBeatmap(author), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Text == "mapper_name")); AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Text == "mapper_name"));
} }
@ -45,7 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestMapWithUnknownMapper() public void TestMapWithUnknownMapper()
{ {
AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore())); AddStep("show example score", () => showPanel(createTestBeatmap(null), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
AddAssert("mapped by text not present", () => AddAssert("mapped by text not present", () =>
this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text, "mapped", "by")));
@ -66,29 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking
return new TestWorkingBeatmap(beatmap); return new TestWorkingBeatmap(beatmap);
} }
private ScoreInfo createTestScore() => new ScoreInfo
{
User = new User
{
Id = 2,
Username = "peppy",
},
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
TotalScore = 999999,
Accuracy = 0.95,
MaxCombo = 999,
Rank = ScoreRank.S,
Date = DateTimeOffset.Now,
Statistics =
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
{ HitResult.Great, 300 },
}
};
private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains);
private class ExpandedPanelMiddleContentContainer : Container private class ExpandedPanelMiddleContentContainer : Container

View File

@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking.Expanded; using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Users;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#444"), Colour = Color4Extensions.FromHex("#444"),
}, },
new ExpandedPanelTopContent(new User { Id = 2, Username = "peppy" }), new ExpandedPanelTopContent(new TestScoreInfo(new OsuRuleset().RulesetInfo).User),
} }
}; };
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -11,13 +9,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
{ {
@ -41,26 +36,7 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
} }
private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo private TestSoloResults createResultsScreen() => new TestSoloResults(new TestScoreInfo(new OsuRuleset().RulesetInfo));
{
TotalScore = 2845370,
Accuracy = 0.98,
MaxCombo = 123,
Rank = ScoreRank.A,
Date = DateTimeOffset.Now,
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Great, 50 },
{ HitResult.Good, 20 },
{ HitResult.Meh, 50 },
{ HitResult.Miss, 1 }
},
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
User = new User
{
Username = "peppy",
}
});
[Test] [Test]
public void ResultsWithoutPlayer() public void ResultsWithoutPlayer()

View File

@ -1,28 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
{ {
public class TestSceneScorePanel : OsuTestScene public class TestSceneScorePanel : OsuTestScene
{ {
private ScorePanel panel;
[Test] [Test]
public void TestDRank() public void TestDRank()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D };
score.Accuracy = 0.5;
score.Rank = ScoreRank.D;
addPanelStep(score); addPanelStep(score);
} }
@ -30,9 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestCRank() public void TestCRank()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C };
score.Accuracy = 0.75;
score.Rank = ScoreRank.C;
addPanelStep(score); addPanelStep(score);
} }
@ -40,9 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestBRank() public void TestBRank()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B };
score.Accuracy = 0.85;
score.Rank = ScoreRank.B;
addPanelStep(score); addPanelStep(score);
} }
@ -50,9 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestARank() public void TestARank()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
score.Accuracy = 0.925;
score.Rank = ScoreRank.A;
addPanelStep(score); addPanelStep(score);
} }
@ -60,9 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestSRank() public void TestSRank()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S };
score.Accuracy = 0.975;
score.Rank = ScoreRank.S;
addPanelStep(score); addPanelStep(score);
} }
@ -70,9 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestAlmostSSRank() public void TestAlmostSSRank()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S };
score.Accuracy = 0.9999;
score.Rank = ScoreRank.S;
addPanelStep(score); addPanelStep(score);
} }
@ -80,9 +65,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestSSRank() public void TestSSRank()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X };
score.Accuracy = 1;
score.Rank = ScoreRank.X;
addPanelStep(score); addPanelStep(score);
} }
@ -90,44 +73,42 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestAllHitResults() public void TestAllHitResults()
{ {
var score = createScore(); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } };
score.Statistics[HitResult.Perfect] = 350;
score.Statistics[HitResult.Ok] = 200;
addPanelStep(score); addPanelStep(score);
} }
private void addPanelStep(ScoreInfo score) => AddStep("add panel", () => [Test]
public void TestContractedPanel()
{ {
Child = new ScorePanel(score) var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
addPanelStep(score, PanelState.Contracted);
}
[Test]
public void TestExpandAndContract()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
addPanelStep(score, PanelState.Contracted);
AddWaitStep("wait for transition", 10);
AddStep("expand panel", () => panel.State = PanelState.Expanded);
AddWaitStep("wait for transition", 10);
AddStep("contract panel", () => panel.State = PanelState.Contracted);
AddWaitStep("wait for transition", 10);
}
private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () =>
{
Child = panel = new ScorePanel(score)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
State = PanelState.Expanded State = state
}; };
}); });
private ScoreInfo createScore() => new ScoreInfo
{
User = new User
{
Id = 2,
Username = "peppy",
},
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
TotalScore = 2845370,
Accuracy = 0.95,
MaxCombo = 999,
Rank = ScoreRank.S,
Date = DateTimeOffset.Now,
Statistics =
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
{ HitResult.Great, 300 },
}
};
} }
} }

View File

@ -0,0 +1,208 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneScorePanelList : OsuManualInputManagerTestScene
{
private ScorePanelList list;
[Test]
public void TestEmptyList()
{
createListStep(() => new ScorePanelList());
}
[Test]
public void TestEmptyListWithSelectedScore()
{
createListStep(() => new ScorePanelList
{
SelectedScore = { Value = new TestScoreInfo(new OsuRuleset().RulesetInfo) }
});
}
[Test]
public void TestAddPanelAfterSelectingScore()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() => new ScorePanelList
{
SelectedScore = { Value = score }
});
AddStep("add panel", () => list.AddScore(score));
assertScoreState(score, true);
assertExpandedPanelCentred();
}
[Test]
public void TestAddPanelBeforeSelectingScore()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() => new ScorePanelList());
AddStep("add panel", () => list.AddScore(score));
assertScoreState(score, false);
assertFirstPanelCentred();
AddStep("select score", () => list.SelectedScore.Value = score);
assertScoreState(score, true);
assertExpandedPanelCentred();
}
[Test]
public void TestAddManyNonExpandedPanels()
{
createListStep(() => new ScorePanelList());
AddStep("add many scores", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo));
});
assertFirstPanelCentred();
}
[Test]
public void TestAddManyScoresAfterExpandedPanel()
{
var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() => new ScorePanelList());
AddStep("add initial panel and select", () =>
{
list.AddScore(initialScore);
list.SelectedScore.Value = initialScore;
});
AddStep("add many scores", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
});
assertScoreState(initialScore, true);
assertExpandedPanelCentred();
}
[Test]
public void TestAddManyScoresBeforeExpandedPanel()
{
var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() => new ScorePanelList());
AddStep("add initial panel and select", () =>
{
list.AddScore(initialScore);
list.SelectedScore.Value = initialScore;
});
AddStep("add scores", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
});
assertScoreState(initialScore, true);
assertExpandedPanelCentred();
}
[Test]
public void TestAddManyPanelsOnBothSidesOfExpandedPanel()
{
var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() => new ScorePanelList());
AddStep("add initial panel and select", () =>
{
list.AddScore(initialScore);
list.SelectedScore.Value = initialScore;
});
AddStep("add scores after", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
});
assertScoreState(initialScore, true);
assertExpandedPanelCentred();
}
[Test]
public void TestSelectMultipleScores()
{
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() => new ScorePanelList());
AddStep("add scores and select first", () =>
{
list.AddScore(firstScore);
list.AddScore(secondScore);
list.SelectedScore.Value = firstScore;
});
assertScoreState(firstScore, true);
assertScoreState(secondScore, false);
AddStep("select second score", () =>
{
InputManager.MoveMouseTo(list.ChildrenOfType<ScorePanel>().Single(p => p.Score == secondScore));
InputManager.Click(MouseButton.Left);
});
assertScoreState(firstScore, false);
assertScoreState(secondScore, true);
assertExpandedPanelCentred();
}
private void createListStep(Func<ScorePanelList> creationFunc)
{
AddStep("create list", () => Child = list = creationFunc().With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}));
AddUntilStep("wait for load", () => list.IsLoaded);
}
private void assertExpandedPanelCentred() => AddUntilStep("expanded panel centred", () =>
{
var expandedPanel = list.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1);
});
private void assertFirstPanelCentred()
=> AddUntilStep("first panel centred", () => Precision.AlmostEquals(list.ChildrenOfType<ScorePanel>().First().ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1));
private void assertScoreState(ScoreInfo score, bool expanded)
=> AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType<ScorePanel>().Single(p => p.Score == score).State == PanelState.Expanded) == expanded);
}
}

View File

@ -233,9 +233,9 @@ namespace osu.Game.Beatmaps.Formats
writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
if (hitObject is IHasCurve curveData) if (hitObject is IHasPath path)
{ {
addCurveData(writer, curveData, position); addPathData(writer, path, position);
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
} }
else else
@ -263,7 +263,7 @@ namespace osu.Game.Beatmaps.Formats
switch (hitObject) switch (hitObject)
{ {
case IHasCurve _: case IHasPath _:
type |= LegacyHitObjectType.Slider; type |= LegacyHitObjectType.Slider;
break; break;
@ -282,13 +282,13 @@ namespace osu.Game.Beatmaps.Formats
return type; return type;
} }
private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position) private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position)
{ {
PathType? lastType = null; PathType? lastType = null;
for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) for (int i = 0; i < pathData.Path.ControlPoints.Count; i++)
{ {
PathControlPoint point = curveData.Path.ControlPoints[i]; PathControlPoint point = pathData.Path.ControlPoints[i];
if (point.Type.Value != null) if (point.Type.Value != null)
{ {
@ -325,13 +325,17 @@ namespace osu.Game.Beatmaps.Formats
if (i != 0) if (i != 0)
{ {
writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}")); writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}"));
writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); writer.Write(i != pathData.Path.ControlPoints.Count - 1 ? "|" : ",");
} }
} }
writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); var curveData = pathData as IHasPathWithRepeats;
writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},"));
writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},"));
writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},"));
if (curveData != null)
{
for (int i = 0; i < curveData.NodeSamples.Count; i++) for (int i = 0; i < curveData.NodeSamples.Count; i++)
{ {
writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}"));
@ -344,6 +348,7 @@ namespace osu.Game.Beatmaps.Formats
writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
} }
} }
}
private void addEndTimeData(TextWriter writer, HitObject hitObject) private void addEndTimeData(TextWriter writer, HitObject hitObject)
{ {

View File

@ -27,7 +27,15 @@ namespace osu.Game.Online.API.Requests
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) public SearchBeatmapSetsRequest(
string query,
RulesetInfo ruleset,
Cursor cursor = null,
SearchCategory searchCategory = SearchCategory.Any,
SortCriteria sortCriteria = SortCriteria.Ranked,
SortDirection sortDirection = SortDirection.Descending,
SearchGenre genre = SearchGenre.Any,
SearchLanguage language = SearchLanguage.Any)
{ {
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset; this.ruleset = ruleset;
@ -36,8 +44,8 @@ namespace osu.Game.Online.API.Requests
SearchCategory = searchCategory; SearchCategory = searchCategory;
SortCriteria = sortCriteria; SortCriteria = sortCriteria;
SortDirection = sortDirection; SortDirection = sortDirection;
Genre = SearchGenre.Any; Genre = genre;
Language = SearchLanguage.Any; Language = language;
} }
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()

View File

@ -177,7 +177,9 @@ namespace osu.Game.Overlays.BeatmapListing
lastResponse?.Cursor, lastResponse?.Cursor,
searchControl.Category.Value, searchControl.Category.Value,
sortControl.Current.Value, sortControl.Current.Value,
sortControl.SortDirection.Value); sortControl.SortDirection.Value,
searchControl.Genre.Value,
searchControl.Language.Value);
getSetsRequest.Success += response => getSetsRequest.Success += response =>
{ {
@ -186,6 +188,9 @@ namespace osu.Game.Overlays.BeatmapListing
if (sets.Count == 0) if (sets.Count == 0)
noMoreResults = true; noMoreResults = true;
if (CurrentPage == 0)
searchControl.BeatmapSet = sets.FirstOrDefault();
lastResponse = response; lastResponse = response;
getSetsRequest = null; getSetsRequest = null;

View File

@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.Edit
protected ComposeBlueprintContainer BlueprintContainer { get; private set; } protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
public override Playfield Playfield => drawableRulesetWrapper.Playfield;
private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper; private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper;
protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
@ -61,7 +59,6 @@ namespace osu.Game.Rulesets.Edit
protected HitObjectComposer(Ruleset ruleset) protected HitObjectComposer(Ruleset ruleset)
{ {
Ruleset = ruleset; Ruleset = ruleset;
RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -137,6 +134,49 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
} }
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
public override Playfield Playfield => drawableRulesetWrapper.Playfield;
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
/// <summary>
/// Defines all available composition tools, listed on the left side of the editor screen as button controls.
/// This should usually define one tool for each <see cref="HitObject"/> type used in the target ruleset.
/// </summary>
/// <remarks>
/// A "select" tool is automatically added as the first tool.
/// </remarks>
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
/// </summary>
protected abstract ComposeBlueprintContainer CreateBlueprintContainer();
/// <summary>
/// Construct a drawable ruleset for the provided ruleset.
/// </summary>
/// <remarks>
/// Can be overridden to add editor-specific logical changes to a <see cref="Ruleset"/>'s standard <see cref="DrawableRuleset{TObject}"/>.
/// For example, hit animations or judgement logic may be changed to give a better editor user experience.
/// </remarks>
/// <param name="ruleset">The ruleset used to construct its drawable counterpart.</param>
/// <param name="beatmap">The loaded beatmap.</param>
/// <param name="mods">The mods to be applied.</param>
/// <returns>An editor-relevant <see cref="DrawableRuleset{TObject}"/>.</returns>
protected virtual DrawableRuleset<TObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
=> (DrawableRuleset<TObject>)ruleset.CreateDrawableRulesetWith(beatmap, mods);
#region Tool selection logic
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.Key >= Key.Number1 && e.Key <= Key.Number9) if (e.Key >= Key.Number1 && e.Key <= Key.Number9)
@ -153,13 +193,6 @@ namespace osu.Game.Rulesets.Edit
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs)
{ {
if (EditorBeatmap.SelectedHitObjects.Any()) if (EditorBeatmap.SelectedHitObjects.Any())
@ -179,15 +212,9 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap.SelectedHitObjects.Clear(); EditorBeatmap.SelectedHitObjects.Clear();
} }
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; #endregion
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); #region IPlacementHandler
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
protected abstract ComposeBlueprintContainer CreateBlueprintContainer();
protected abstract DrawableRuleset<TObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null);
public void BeginPlacement(HitObject hitObject) public void BeginPlacement(HitObject hitObject)
{ {
@ -209,6 +236,17 @@ namespace osu.Game.Rulesets.Edit
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
#endregion
#region IPositionSnapProvider
/// <summary>
/// Retrieve the relevant <see cref="Playfield"/> at a specified screen-space position.
/// In cases where a ruleset doesn't require custom logic (due to nested playfields, for example)
/// this will return the ruleset's main playfield.
/// </summary>
/// <param name="screenSpacePosition">The screen-space position to query.</param>
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
@ -257,8 +295,14 @@ namespace osu.Game.Rulesets.Edit
return DurationToDistance(referenceTime, snappedEndTime - referenceTime); return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
} }
#endregion
} }
/// <summary>
/// A non-generic definition of a HitObject composer class.
/// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />.
/// </summary>
[Cached(typeof(HitObjectComposer))] [Cached(typeof(HitObjectComposer))]
[Cached(typeof(IPositionSnapProvider))] [Cached(typeof(IPositionSnapProvider))]
public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
@ -268,10 +312,13 @@ namespace osu.Game.Rulesets.Edit
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
/// <summary>
/// The target ruleset's playfield.
/// </summary>
public abstract Playfield Playfield { get; } public abstract Playfield Playfield { get; }
/// <summary> /// <summary>
/// All the <see cref="DrawableHitObject"/>s. /// All <see cref="DrawableHitObject"/>s in currently loaded beatmap.
/// </summary> /// </summary>
public abstract IEnumerable<DrawableHitObject> HitObjects { get; } public abstract IEnumerable<DrawableHitObject> HitObjects { get; }
@ -280,6 +327,8 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public abstract bool CursorInPlacementArea { get; } public abstract bool CursorInPlacementArea { get; }
#region IPositionSnapProvider
public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float GetBeatSnapDistanceAt(double referenceTime);
@ -291,5 +340,7 @@ namespace osu.Game.Rulesets.Edit
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
#endregion
} }
} }

View File

@ -9,7 +9,10 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
{ {
internal abstract class ConvertSlider : ConvertHitObject, IHasCurve, IHasLegacyLastTickOffset internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset,
#pragma warning disable 618
IHasCurve
#pragma warning restore 618
{ {
/// <summary> /// <summary>
/// Scoring distance with a speed-adjusted beat length of 1 second. /// Scoring distance with a speed-adjusted beat length of 1 second.

View File

@ -1,13 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Objects.Types namespace osu.Game.Rulesets.Objects.Types
{ {
/// <summary> [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
/// A HitObject that has a curve.
/// </summary>
public interface IHasCurve : IHasDistance, IHasRepeats public interface IHasCurve : IHasDistance, IHasRepeats
{ {
/// <summary> /// <summary>
@ -16,6 +15,8 @@ namespace osu.Game.Rulesets.Objects.Types
SliderPath Path { get; } SliderPath Path { get; }
} }
#pragma warning disable 618
[Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
public static class HasCurveExtensions public static class HasCurveExtensions
{ {
/// <summary> /// <summary>
@ -50,4 +51,5 @@ namespace osu.Game.Rulesets.Objects.Types
public static int SpanAt(this IHasCurve obj, double progress) public static int SpanAt(this IHasCurve obj, double progress)
=> (int)(progress * obj.SpanCount()); => (int)(progress * obj.SpanCount());
} }
#pragma warning restore 618
} }

View File

@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Rulesets.Objects.Types
{
public interface IHasPath : IHasDistance
{
/// <summary>
/// The curve.
/// </summary>
SliderPath Path { get; }
}
}

View File

@ -0,0 +1,50 @@
// 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 osuTK;
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// A HitObject that has a curve.
/// </summary>
// ReSharper disable once RedundantExtendsListEntry
public interface IHasPathWithRepeats : IHasPath, IHasRepeats
{
}
public static class HasPathWithRepeatsExtensions
{
/// <summary>
/// Computes the position on the curve relative to how much of the <see cref="HitObject"/> has been completed.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
/// <returns>The position on the curve.</returns>
public static Vector2 CurvePositionAt(this IHasPathWithRepeats obj, double progress)
=> obj.Path.PositionAt(obj.ProgressAt(progress));
/// <summary>
/// Computes the progress along the curve relative to how much of the <see cref="HitObject"/> has been completed.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
/// <returns>[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</returns>
public static double ProgressAt(this IHasPathWithRepeats obj, double progress)
{
double p = progress * obj.SpanCount() % 1;
if (obj.SpanAt(progress) % 2 == 1)
p = 1 - p;
return p;
}
/// <summary>
/// Determines which span of the curve the progress point is on.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</param>
/// <returns>[0, SpanCount) where 0 is the first run.</returns>
public static int SpanAt(this IHasPathWithRepeats obj, double progress)
=> (int)(progress * obj.SpanCount());
}
}

View File

@ -479,7 +479,7 @@ namespace osu.Game.Screens.Play
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score); protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score);
#region Fail Logic #region Fail Logic

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play
this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo));
} }
protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false); protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
protected override ScoreInfo CreateScore() => score.ScoreInfo; protected override ScoreInfo CreateScore() => score.ScoreInfo;
} }

View File

@ -0,0 +1,222 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
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.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Ranking.Contracted
{
/// <summary>
/// The content that appears in the middle of a contracted <see cref="ScorePanel"/>.
/// </summary>
public class ContractedPanelMiddleContent : CompositeDrawable
{
private readonly ScoreInfo score;
/// <summary>
/// Creates a new <see cref="ContractedPanelMiddleContent"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to display.</param>
public ContractedPanelMiddleContent(ScoreInfo score)
{
this.score = score;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerExponent = 2.5f,
CornerRadius = 20,
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 1,
Offset = new Vector2(0, 4)
},
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("444")
},
new UserCoverBackground
{
RelativeSizeAxes = Axes.Both,
User = score.User,
Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0))
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new UpdateableAvatar(score.User)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(110),
Masking = true,
CornerExponent = 2.5f,
CornerRadius = 20,
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.25f),
Type = EdgeEffectType.Shadow,
Radius = 8,
Offset = new Vector2(0, 4),
}
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = score.UserString,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold)
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString()))
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new[]
{
createStatistic("Max Combo", $"x{score.MaxCombo}"),
createStatistic("Accuracy", $"{score.Accuracy.FormatAccuracy()}"),
}
},
new ModDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
ExpansionMode = ExpansionMode.AlwaysExpanded,
DisplayUnrankedText = false,
Current = { Value = score.Mods },
Scale = new Vector2(0.5f),
}
}
}
}
},
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 5 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = score.TotalScore.ToString("N0"),
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true),
Spacing = new Vector2(-1, 0)
},
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 2 },
Child = new DrawableRank(score.Rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
}
}
}
},
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 45),
}
};
}
private Drawable createStatistic(string key, string value) => new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = key,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)
},
new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Text = value,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Colour = Color4Extensions.FromHex("#FFDD55")
}
}
};
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -13,6 +14,8 @@ namespace osu.Game.Screens.Ranking
{ {
public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager> public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager>
{ {
public Bindable<ScoreInfo> Score => Model;
private DownloadButton button; private DownloadButton button;
private ShakeContainer shakeContainer; private ShakeContainer shakeContainer;
@ -23,7 +26,7 @@ namespace osu.Game.Screens.Ranking
if (State.Value == DownloadState.LocallyAvailable) if (State.Value == DownloadState.LocallyAvailable)
return ReplayAvailability.Local; return ReplayAvailability.Local;
if (!string.IsNullOrEmpty(Model.Value.Hash)) if (!string.IsNullOrEmpty(Model.Value?.Hash))
return ReplayAvailability.Online; return ReplayAvailability.Online;
return ReplayAvailability.NotAvailable; return ReplayAvailability.NotAvailable;

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -10,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -17,7 +20,7 @@ using osuTK;
namespace osu.Game.Screens.Ranking namespace osu.Game.Screens.Ranking
{ {
public class ResultsScreen : OsuScreen public abstract class ResultsScreen : OsuScreen
{ {
protected const float BACKGROUND_BLUR = 20; protected const float BACKGROUND_BLUR = 20;
@ -28,19 +31,26 @@ namespace osu.Game.Screens.Ranking
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value);
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
public readonly ScoreInfo Score;
private readonly bool allowRetry;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Player player { get; set; } private Player player { get; set; }
public readonly ScoreInfo Score; [Resolved]
private IAPIProvider api { get; set; }
private readonly bool allowRetry;
private Drawable bottomPanel; private Drawable bottomPanel;
private ScorePanelList panels;
public ResultsScreen(ScoreInfo score, bool allowRetry = true) protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
{ {
Score = score; Score = score;
this.allowRetry = allowRetry; this.allowRetry = allowRetry;
SelectedScore.Value = score;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -48,17 +58,24 @@ namespace osu.Game.Screens.Ranking
{ {
FillFlowContainer buttons; FillFlowContainer buttons;
InternalChildren = new[] InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{ {
new ResultsScrollContainer new ResultsScrollContainer
{ {
Child = new ScorePanel(Score) Child = panels = new ScorePanelList
{ {
Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre, SelectedScore = { BindTarget = SelectedScore }
State = PanelState.Expanded }
}, }
}, },
new[]
{
bottomPanel = new Container bottomPanel = new Container
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
@ -82,13 +99,27 @@ namespace osu.Game.Screens.Ranking
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
new ReplayDownloadButton(Score) { Width = 300 }, new ReplayDownloadButton(null)
{
Score = { BindTarget = SelectedScore },
Width = 300
},
} }
} }
} }
} }
}
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
}
}; };
if (Score != null)
panels.AddScore(Score);
if (player != null && allowRetry) if (player != null && allowRetry)
{ {
buttons.Add(new RetryButton { Width = 300 }); buttons.Add(new RetryButton { Width = 300 });
@ -105,6 +136,27 @@ namespace osu.Game.Screens.Ranking
} }
} }
protected override void LoadComplete()
{
base.LoadComplete();
var req = FetchScores(scores => Schedule(() =>
{
foreach (var s in scores)
panels.AddScore(s);
}));
if (req != null)
api.Queue(req);
}
/// <summary>
/// Performs a fetch/refresh of scores to be displayed.
/// </summary>
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
protected virtual APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
@ -142,7 +194,7 @@ namespace osu.Game.Screens.Ranking
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
content.Height = Math.Max(768, DrawHeight); content.Height = Math.Max(768 - TwoLayerButton.SIZE_EXTENDED.Y, DrawHeight);
} }
} }
} }

View File

@ -9,7 +9,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Screens.Ranking.Expanded; using osu.Game.Screens.Ranking.Expanded;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -21,12 +23,12 @@ namespace osu.Game.Screens.Ranking
/// <summary> /// <summary>
/// Width of the panel when contracted. /// Width of the panel when contracted.
/// </summary> /// </summary>
private const float contracted_width = 160; public const float CONTRACTED_WIDTH = 130;
/// <summary> /// <summary>
/// Height of the panel when contracted. /// Height of the panel when contracted.
/// </summary> /// </summary>
private const float contracted_height = 320; private const float contracted_height = 355;
/// <summary> /// <summary>
/// Width of the panel when expanded. /// Width of the panel when expanded.
@ -46,7 +48,7 @@ namespace osu.Game.Screens.Ranking
/// <summary> /// <summary>
/// Height of the top layer when the panel is contracted. /// Height of the top layer when the panel is contracted.
/// </summary> /// </summary>
private const float contracted_top_layer_height = 40; private const float contracted_top_layer_height = 30;
/// <summary> /// <summary>
/// Duration for the panel to resize into its expanded/contracted size. /// Duration for the panel to resize into its expanded/contracted size.
@ -71,11 +73,12 @@ namespace osu.Game.Screens.Ranking
private static readonly ColourInfo expanded_top_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#444"), Color4Extensions.FromHex("#333")); private static readonly ColourInfo expanded_top_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#444"), Color4Extensions.FromHex("#333"));
private static readonly ColourInfo expanded_middle_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")); private static readonly ColourInfo expanded_middle_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333"));
private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535");
private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#444"); private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535");
public event Action<PanelState> StateChanged; public event Action<PanelState> StateChanged;
public readonly ScoreInfo Score;
private readonly ScoreInfo score; private Container content;
private Container topLayerContainer; private Container topLayerContainer;
private Drawable topLayerBackground; private Drawable topLayerBackground;
@ -89,18 +92,24 @@ namespace osu.Game.Screens.Ranking
public ScorePanel(ScoreInfo score) public ScorePanel(ScoreInfo score)
{ {
this.score = score; Score = score;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChildren = new Drawable[] InternalChild = content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(40),
Children = new Drawable[]
{ {
topLayerContainer = new Container topLayerContainer = new Container
{ {
Name = "Top layer", Name = "Top layer",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Alpha = 0,
Height = 120, Height = 120,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -132,6 +141,7 @@ namespace osu.Game.Screens.Ranking
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
} }
} }
}
}; };
} }
@ -174,34 +184,40 @@ namespace osu.Game.Screens.Ranking
private void updateState() private void updateState()
{ {
topLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint);
middleLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint);
topLayerContent?.FadeOut(content_fade_duration).Expire(); topLayerContent?.FadeOut(content_fade_duration).Expire();
middleLayerContent?.FadeOut(content_fade_duration).Expire(); middleLayerContent?.FadeOut(content_fade_duration).Expire();
switch (state) switch (state)
{ {
case PanelState.Expanded: case PanelState.Expanded:
this.ResizeTo(new Vector2(EXPANDED_WIDTH, expanded_height), resize_duration, Easing.OutQuint); Size = new Vector2(EXPANDED_WIDTH, expanded_height);
topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(score.User).With(d => d.Alpha = 0)); topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(score).With(d => d.Alpha = 0)); middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0));
break; break;
case PanelState.Contracted: case PanelState.Contracted:
this.ResizeTo(new Vector2(contracted_width, contracted_height), resize_duration, Easing.OutQuint); Size = new Vector2(CONTRACTED_WIDTH, contracted_height);
topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint); topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint);
middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint);
middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0));
break; break;
} }
using (BeginDelayedSequence(resize_duration + top_layer_expand_delay, true)) content.ResizeTo(Size, resize_duration, Easing.OutQuint);
bool topLayerExpanded = topLayerContainer.Y < 0;
// If the top layer was already expanded, then we don't need to wait for the resize and can instead transform immediately. This looks better when changing the panel state.
using (BeginDelayedSequence(topLayerExpanded ? 0 : resize_duration + top_layer_expand_delay, true))
{ {
topLayerContainer.FadeIn();
switch (state) switch (state)
{ {
case PanelState.Expanded: case PanelState.Expanded:
@ -219,5 +235,13 @@ namespace osu.Game.Screens.Ranking
middleLayerContent?.FadeIn(content_fade_duration); middleLayerContent?.FadeIn(content_fade_duration);
} }
} }
protected override bool OnClick(ClickEvent e)
{
if (State == PanelState.Contracted)
State = PanelState.Expanded;
return true;
}
} }
} }

View File

@ -0,0 +1,185 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Screens.Ranking
{
public class ScorePanelList : CompositeDrawable
{
/// <summary>
/// Normal spacing between all panels.
/// </summary>
private const float panel_spacing = 5;
/// <summary>
/// Spacing around both sides of the expanded panel. This is added on top of <see cref="panel_spacing"/>.
/// </summary>
private const float expanded_panel_spacing = 15;
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
private readonly Flow flow;
private readonly Scroll scroll;
private ScorePanel expandedPanel;
public ScorePanelList()
{
RelativeSizeAxes = Axes.Both;
InternalChild = scroll = new Scroll
{
RelativeSizeAxes = Axes.Both,
Child = flow = new Flow
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(panel_spacing, 0),
AutoSizeAxes = Axes.Both,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedScore.BindValueChanged(selectedScoreChanged, true);
}
/// <summary>
/// Adds a <see cref="ScoreInfo"/> to this list.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to add.</param>
public void AddScore(ScoreInfo score)
{
flow.Add(new ScorePanel(score)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}.With(p =>
{
p.StateChanged += s =>
{
if (s == PanelState.Expanded)
SelectedScore.Value = p.Score;
};
}));
if (SelectedScore.Value == score)
selectedScoreChanged(new ValueChangedEvent<ScoreInfo>(SelectedScore.Value, SelectedScore.Value));
else
{
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
{
// A somewhat hacky property is used here because we need to:
// 1) Scroll after the scroll container's visible range is updated.
// 2) Scroll before the scroll container's scroll position is updated.
// Without this, we would have a 1-frame positioning error which looks very jarring.
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
}
}
}
/// <summary>
/// Brings a <see cref="ScoreInfo"/> to the centre of the screen and expands it.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to present.</param>
private void selectedScoreChanged(ValueChangedEvent<ScoreInfo> score)
{
// Contract the old panel.
foreach (var p in flow.Where(p => p.Score == score.OldValue))
{
p.State = PanelState.Contracted;
p.Margin = new MarginPadding();
}
// Find the panel corresponding to the new score.
expandedPanel = flow.SingleOrDefault(p => p.Score == score.NewValue);
if (expandedPanel == null)
return;
// Expand the new panel.
expandedPanel.State = PanelState.Expanded;
expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing };
// Scroll to the new panel. This is done manually since we need:
// 1) To scroll after the scroll container's visible range is updated.
// 2) To account for the centre anchor/origins of panels.
// In the end, it's easier to compute the scroll position manually.
float scrollOffset = flow.GetPanelIndex(expandedPanel.Score) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing);
scroll.ScrollTo(scrollOffset);
}
protected override void Update()
{
base.Update();
float offset = DrawWidth / 2f;
// Add padding to both sides such that the centre of an expanded panel on either side is in the middle of the screen.
if (SelectedScore.Value != null)
{
// The expanded panel has extra padding applied to it, so it needs to be included into the offset.
offset -= ScorePanel.EXPANDED_WIDTH / 2f + expanded_panel_spacing;
}
else
offset -= ScorePanel.CONTRACTED_WIDTH / 2f;
flow.Padding = new MarginPadding { Horizontal = offset };
}
private class Flow : FillFlowContainer<ScorePanel>
{
public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren);
public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Score != score).Count();
private IEnumerable<ScorePanel> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanel>()
.OrderByDescending(s => s.Score.TotalScore)
.ThenBy(s => s.Score.OnlineScoreID);
}
private class Scroll : OsuScrollContainer
{
public new float Target => base.Target;
public Scroll()
: base(Direction.Horizontal)
{
}
/// <summary>
/// The target that will be scrolled to instantaneously next frame.
/// </summary>
public float? InstantScrollTarget;
protected override void UpdateAfterChildren()
{
if (InstantScrollTarget != null)
{
ScrollTo(InstantScrollTarget.Value, false);
InstantScrollTarget = null;
}
base.UpdateAfterChildren();
}
public override bool HandlePositionalInput => false;
public override bool HandleNonPositionalInput => false;
}
}
}

View File

@ -0,0 +1,32 @@
// 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 osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Scoring;
namespace osu.Game.Screens.Ranking
{
public class SoloResultsScreen : ResultsScreen
{
[Resolved]
private RulesetStore rulesets { get; set; }
public SoloResultsScreen(ScoreInfo score, bool allowRetry = true)
: base(score, allowRetry)
{
}
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{
var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset);
req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets)));
return req;
}
}
}

View File

@ -37,7 +37,7 @@ namespace osu.Game.Screens.Select
} }
protected void PresentScore(ScoreInfo score) => protected void PresentScore(ScoreInfo score) =>
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new ResultsScreen(score))); FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score)));
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();

View File

@ -0,0 +1,52 @@
// 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.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
namespace osu.Game.Tests
{
public class TestScoreInfo : ScoreInfo
{
public TestScoreInfo(RulesetInfo ruleset)
{
User = new User
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
};
Beatmap = new TestBeatmap(ruleset).BeatmapInfo;
Ruleset = ruleset;
RulesetID = ruleset.ID ?? 0;
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() };
TotalScore = 2845370;
Accuracy = 0.95;
MaxCombo = 999;
Rank = ScoreRank.S;
Date = DateTimeOffset.Now;
Statistics[HitResult.Miss] = 1;
Statistics[HitResult.Meh] = 50;
Statistics[HitResult.Good] = 100;
Statistics[HitResult.Great] = 300;
}
private class TestModHardRock : ModHardRock
{
public override double ScoreMultiplier => 1;
}
private class TestModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => 1;
}
}
}

View File

@ -1,4 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Efnt/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Efnt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Emp3/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Emp3/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Epng/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Epng/@EntryIndexedValue">True</s:Boolean>
@ -905,14 +905,17 @@ private void load()
<s:Boolean x:Key="/Default/UserDictionary/Words/=Catmull/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Catmull/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Drawables/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Drawables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gameplay/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=gameplay/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hitobject/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hitobjects/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=hitobjects/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=keymods/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=keymods/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kiai/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Kiai/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Leaderboard/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Leaderboard/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Leaderboards/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Leaderboards/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Playfield/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Playfield/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=playfields/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=resampler/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=resampler/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ruleset/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=ruleset/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rulesets/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=rulesets/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ruleset_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Taiko/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Taiko/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>