1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 08:52:55 +08:00

Merge branch 'master' into editor-create-composer-fix

This commit is contained in:
Dean Herbert 2020-06-01 15:38:15 +09:00 committed by GitHub
commit b9464a2aa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1103 additions and 237 deletions

View File

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

View File

@ -14,7 +14,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
public class JuiceStream : CatchHitObject, IHasCurve
public class JuiceStream : CatchHitObject, IHasPathWithRepeats
{
/// <summary>
/// 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>
private IList<HitSampleInfo> sampleInfoListAt(double time)
{
if (!(HitObject is IHasCurve curveData))
if (!(HitObject is IHasPathWithRepeats curveData))
return HitObject.Samples;
double segmentTime = (EndTime - HitObject.StartTime) / spanCount;

View File

@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
base.Update();
if (leftSprite?.Height > 0)
leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height);
leftSprite.Scale = new Vector2(1, DrawHeight / leftSprite.Height);
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)
{
case IHasCurve curveData:
case IHasPathWithRepeats curveData:
return new Slider
{
StartTime = original.StartTime,

View File

@ -17,7 +17,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class Slider : OsuHitObject, IHasCurve
public class Slider : OsuHitObject, IHasPathWithRepeats
{
public double EndTime
{

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.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);

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
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;

View File

@ -3,9 +3,7 @@
using osu.Game.Rulesets.Objects.Types;
using System;
using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@ -17,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class DrumRoll : TaikoHitObject, IHasCurve
public class DrumRoll : TaikoHitObject, IHasPath
{
/// <summary>
/// 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;
int IHasRepeats.RepeatCount { get => 0; set { } }
List<IList<HitSampleInfo>> IHasRepeats.NodeSamples => new List<IList<HitSampleInfo>>();
SliderPath IHasCurve.Path
SliderPath IHasPath.Path
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
#endregion

View File

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

View File

@ -95,7 +95,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
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;
Assert.IsNotNull(positionData);

View File

@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable));
AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded));
createButton(false);
createButtonNoScore();
}
private void createButton(bool withReplay)
@ -40,6 +41,22 @@ namespace osu.Game.Tests.Visual.Gameplay
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)

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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -14,10 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Expanded;
@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
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"));
}
@ -45,7 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
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", () =>
this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text, "mapped", "by")));
@ -66,29 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking
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 class ExpandedPanelMiddleContentContainer : Container

View File

@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both,
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -11,13 +9,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking
{
@ -41,26 +36,7 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
}
private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
{
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",
}
});
private TestSoloResults createResultsScreen() => new TestSoloResults(new TestScoreInfo(new OsuRuleset().RulesetInfo));
[Test]
public void ResultsWithoutPlayer()

View File

@ -1,28 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneScorePanel : OsuTestScene
{
private ScorePanel panel;
[Test]
public void TestDRank()
{
var score = createScore();
score.Accuracy = 0.5;
score.Rank = ScoreRank.D;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D };
addPanelStep(score);
}
@ -30,9 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestCRank()
{
var score = createScore();
score.Accuracy = 0.75;
score.Rank = ScoreRank.C;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C };
addPanelStep(score);
}
@ -40,9 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestBRank()
{
var score = createScore();
score.Accuracy = 0.85;
score.Rank = ScoreRank.B;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B };
addPanelStep(score);
}
@ -50,9 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestARank()
{
var score = createScore();
score.Accuracy = 0.925;
score.Rank = ScoreRank.A;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
addPanelStep(score);
}
@ -60,9 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSRank()
{
var score = createScore();
score.Accuracy = 0.975;
score.Rank = ScoreRank.S;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S };
addPanelStep(score);
}
@ -70,9 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAlmostSSRank()
{
var score = createScore();
score.Accuracy = 0.9999;
score.Rank = ScoreRank.S;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S };
addPanelStep(score);
}
@ -80,9 +65,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSSRank()
{
var score = createScore();
score.Accuracy = 1;
score.Rank = ScoreRank.X;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X };
addPanelStep(score);
}
@ -90,44 +73,42 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAllHitResults()
{
var score = createScore();
score.Statistics[HitResult.Perfect] = 350;
score.Statistics[HitResult.Ok] = 200;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } };
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,
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)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));
}
else
@ -263,7 +263,7 @@ namespace osu.Game.Beatmaps.Formats
switch (hitObject)
{
case IHasCurve _:
case IHasPath _:
type |= LegacyHitObjectType.Slider;
break;
@ -282,13 +282,13 @@ namespace osu.Game.Beatmaps.Formats
return type;
}
private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position)
private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position)
{
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)
{
@ -325,23 +325,28 @@ namespace osu.Game.Beatmaps.Formats
if (i != 0)
{
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},"));
writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},"));
var curveData = pathData as IHasPathWithRepeats;
for (int i = 0; i < curveData.NodeSamples.Count; i++)
{
writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}"));
writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
}
writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},"));
writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},"));
for (int i = 0; i < curveData.NodeSamples.Count; i++)
if (curveData != null)
{
writer.Write(getSampleBank(curveData.NodeSamples[i], true));
writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
for (int i = 0; i < curveData.NodeSamples.Count; i++)
{
writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}"));
writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
}
for (int i = 0; i < curveData.NodeSamples.Count; i++)
{
writer.Write(getSampleBank(curveData.NodeSamples[i], true));
writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
}
}
}

View File

@ -27,7 +27,15 @@ namespace osu.Game.Online.API.Requests
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.ruleset = ruleset;
@ -36,8 +44,8 @@ namespace osu.Game.Online.API.Requests
SearchCategory = searchCategory;
SortCriteria = sortCriteria;
SortDirection = sortDirection;
Genre = SearchGenre.Any;
Language = SearchLanguage.Any;
Genre = genre;
Language = language;
}
protected override WebRequest CreateWebRequest()

View File

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

View File

@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Edit
/// <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>.
/// <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);

View File

@ -9,7 +9,10 @@ using osu.Game.Beatmaps.ControlPoints;
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>
/// 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.
using System;
using osuTK;
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// A HitObject that has a curve.
/// </summary>
[Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
public interface IHasCurve : IHasDistance, IHasRepeats
{
/// <summary>
@ -16,6 +15,8 @@ namespace osu.Game.Rulesets.Objects.Types
SliderPath Path { get; }
}
#pragma warning disable 618
[Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
public static class HasCurveExtensions
{
/// <summary>
@ -50,4 +51,5 @@ namespace osu.Game.Rulesets.Objects.Types
public static int SpanAt(this IHasCurve obj, double progress)
=> (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 virtual ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score);
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score);
#region Fail Logic

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play
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;
}

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.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@ -13,6 +14,8 @@ namespace osu.Game.Screens.Ranking
{
public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager>
{
public Bindable<ScoreInfo> Score => Model;
private DownloadButton button;
private ShakeContainer shakeContainer;
@ -23,7 +26,7 @@ namespace osu.Game.Screens.Ranking
if (State.Value == DownloadState.LocallyAvailable)
return ReplayAvailability.Local;
if (!string.IsNullOrEmpty(Model.Value.Hash))
if (!string.IsNullOrEmpty(Model.Value?.Hash))
return ReplayAvailability.Online;
return ReplayAvailability.NotAvailable;

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -10,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Scoring;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Play;
@ -17,7 +20,7 @@ using osuTK;
namespace osu.Game.Screens.Ranking
{
public class ResultsScreen : OsuScreen
public abstract class ResultsScreen : OsuScreen
{
protected const float BACKGROUND_BLUR = 20;
@ -28,19 +31,26 @@ namespace osu.Game.Screens.Ranking
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)]
private Player player { get; set; }
public readonly ScoreInfo Score;
private readonly bool allowRetry;
[Resolved]
private IAPIProvider api { get; set; }
private Drawable bottomPanel;
private ScorePanelList panels;
public ResultsScreen(ScoreInfo score, bool allowRetry = true)
protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
{
Score = score;
this.allowRetry = allowRetry;
SelectedScore.Value = score;
}
[BackgroundDependencyLoader]
@ -48,47 +58,68 @@ namespace osu.Game.Screens.Ranking
{
FillFlowContainer buttons;
InternalChildren = new[]
InternalChild = new GridContainer
{
new ResultsScrollContainer
RelativeSizeAxes = Axes.Both,
Content = new[]
{
Child = new ScorePanel(Score)
new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = PanelState.Expanded
new ResultsScrollContainer
{
Child = panels = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore }
}
}
},
},
bottomPanel = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = TwoLayerButton.SIZE_EXTENDED.Y,
Alpha = 0,
Children = new Drawable[]
new[]
{
new Box
bottomPanel = new Container
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333")
},
buttons = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = TwoLayerButton.SIZE_EXTENDED.Y,
Alpha = 0,
Children = new Drawable[]
{
new ReplayDownloadButton(Score) { Width = 300 },
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333")
},
buttons = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
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)
{
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)
{
base.OnEntering(last);
@ -142,7 +194,7 @@ namespace osu.Game.Screens.Ranking
protected override void 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.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Screens.Ranking.Expanded;
using osuTK;
using osuTK.Graphics;
@ -21,12 +23,12 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Width of the panel when contracted.
/// </summary>
private const float contracted_width = 160;
public const float CONTRACTED_WIDTH = 130;
/// <summary>
/// Height of the panel when contracted.
/// </summary>
private const float contracted_height = 320;
private const float contracted_height = 355;
/// <summary>
/// Width of the panel when expanded.
@ -46,7 +48,7 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Height of the top layer when the panel is contracted.
/// </summary>
private const float contracted_top_layer_height = 40;
private const float contracted_top_layer_height = 30;
/// <summary>
/// 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_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_middle_layer_colour = Color4Extensions.FromHex("#444");
private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535");
public event Action<PanelState> StateChanged;
public readonly ScoreInfo Score;
private readonly ScoreInfo score;
private Container content;
private Container topLayerContainer;
private Drawable topLayerBackground;
@ -89,47 +92,54 @@ namespace osu.Game.Screens.Ranking
public ScorePanel(ScoreInfo score)
{
this.score = score;
Score = score;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
InternalChild = content = new Container
{
topLayerContainer = new Container
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(40),
Children = new Drawable[]
{
Name = "Top layer",
RelativeSizeAxes = Axes.X,
Height = 120,
Children = new Drawable[]
topLayerContainer = new Container
{
new Container
Name = "Top layer",
RelativeSizeAxes = Axes.X,
Alpha = 0,
Height = 120,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 20,
CornerExponent = 2.5f,
Masking = true,
Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both }
},
topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
}
},
middleLayerContainer = new Container
{
Name = "Middle layer",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 20,
CornerExponent = 2.5f,
Masking = true,
Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both }
},
topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
}
},
middleLayerContainer = new Container
{
new Container
Name = "Middle layer",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 20,
CornerExponent = 2.5f,
Masking = true,
Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }
},
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 20,
CornerExponent = 2.5f,
Masking = true,
Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }
},
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
}
}
}
};
@ -174,34 +184,40 @@ namespace osu.Game.Screens.Ranking
private void updateState()
{
topLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint);
middleLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint);
topLayerContent?.FadeOut(content_fade_duration).Expire();
middleLayerContent?.FadeOut(content_fade_duration).Expire();
switch (state)
{
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);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(score.User).With(d => d.Alpha = 0));
middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(score).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));
break;
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);
middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint);
middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0));
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)
{
case PanelState.Expanded:
@ -219,5 +235,13 @@ namespace osu.Game.Screens.Ranking
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) =>
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();

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