mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:12:54 +08:00
Merge pull request #14919 from smoogipoo/realtime-pp-display
Implement real-time PP counter
This commit is contained in:
commit
6f7b8293af
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
Mods = mods,
|
||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||
Skills = skills
|
||||
};
|
||||
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
||||
{
|
||||
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
|
||||
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods
|
||||
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
|
||||
private double getScoreMultiplier(Mod[] mods)
|
||||
{
|
||||
double scoreMultiplier = 1;
|
||||
|
||||
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||
var maniaBeatmap = (ManiaBeatmap)Beatmap;
|
||||
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
|
||||
|
||||
if (diff > 0)
|
||||
|
@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
||||
{
|
||||
CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
}
|
||||
|
||||
@ -84,18 +85,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Remove(expectedComponentsAdjustmentContainer);
|
||||
|
||||
return almostEqual(actualInfo, expectedInfo);
|
||||
|
||||
static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
|
||||
other != null
|
||||
&& info.Type == other.Type
|
||||
&& info.Anchor == other.Anchor
|
||||
&& info.Origin == other.Origin
|
||||
&& Precision.AlmostEquals(info.Position, other.Position)
|
||||
&& Precision.AlmostEquals(info.Scale, other.Scale)
|
||||
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
|
||||
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
|
||||
}
|
||||
|
||||
private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
|
||||
other != null
|
||||
&& info.Type == other.Type
|
||||
&& info.Anchor == other.Anchor
|
||||
&& info.Origin == other.Origin
|
||||
&& Precision.AlmostEquals(info.Position, other.Position, 1)
|
||||
&& Precision.AlmostEquals(info.Scale, other.Scale)
|
||||
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
|
||||
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
=> new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin);
|
||||
|
||||
|
@ -0,0 +1,108 @@
|
||||
// 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.Diagnostics;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestScenePerformancePointsCounter : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private GameplayState gameplayState;
|
||||
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor;
|
||||
|
||||
private int iteration;
|
||||
private PerformancePointsCounter counter;
|
||||
|
||||
public TestScenePerformancePointsCounter()
|
||||
{
|
||||
var ruleset = CreateRuleset();
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo)
|
||||
.GetPlayableBeatmap(ruleset.RulesetInfo);
|
||||
|
||||
gameplayState = new GameplayState(beatmap, ruleset);
|
||||
scoreProcessor = new ScoreProcessor();
|
||||
}
|
||||
|
||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create counter", () =>
|
||||
{
|
||||
iteration = 0;
|
||||
|
||||
Child = counter = new PerformancePointsCounter
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(5),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicCounting()
|
||||
{
|
||||
int previousValue = 0;
|
||||
|
||||
AddAssert("counter displaying zero", () => counter.Current.Value == 0);
|
||||
|
||||
AddRepeatStep("Add judgement", applyOneJudgement, 10);
|
||||
|
||||
AddUntilStep("counter non-zero", () => counter.Current.Value > 0);
|
||||
AddUntilStep("counter opaque", () => counter.Child.Alpha == 1);
|
||||
|
||||
AddStep("Revert judgement", () =>
|
||||
{
|
||||
previousValue = counter.Current.Value;
|
||||
|
||||
scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement()));
|
||||
});
|
||||
|
||||
AddUntilStep("counter decreased", () => counter.Current.Value < previousValue);
|
||||
|
||||
AddStep("Add judgement", applyOneJudgement);
|
||||
|
||||
AddUntilStep("counter non-zero", () => counter.Current.Value > 0);
|
||||
}
|
||||
|
||||
private void applyOneJudgement()
|
||||
{
|
||||
var scoreInfo = gameplayState.Score.ScoreInfo;
|
||||
|
||||
scoreInfo.MaxCombo = iteration * 1000;
|
||||
scoreInfo.Accuracy = 1;
|
||||
scoreInfo.Statistics[HitResult.Great] = iteration * 1000;
|
||||
|
||||
scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject
|
||||
{
|
||||
StartTime = iteration * 10000,
|
||||
}, new OsuJudgement())
|
||||
{
|
||||
Type = HitResult.Perfect,
|
||||
});
|
||||
|
||||
iteration++;
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
@ -147,6 +148,14 @@ namespace osu.Game.Beatmaps
|
||||
}, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
|
||||
}
|
||||
|
||||
public Task<List<TimedDifficultyAttributes>> GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default)
|
||||
{
|
||||
return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods),
|
||||
token,
|
||||
TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously,
|
||||
updateScheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="DifficultyRating"/> that describes a star rating.
|
||||
/// </summary>
|
||||
|
@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>The applicable <see cref="IBeatmapConverter"/>.</returns>
|
||||
protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap);
|
||||
|
||||
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
|
||||
public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
|
||||
{
|
||||
using (var cancellationSource = createCancellationTokenSource(timeout))
|
||||
{
|
||||
|
@ -25,7 +25,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private SpriteText displayedCountSpriteText;
|
||||
private IHasText displayedCountText;
|
||||
|
||||
public Drawable DrawableCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the roll-up duration will be proportional to change in value.
|
||||
@ -72,16 +74,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
displayedCountSpriteText = CreateSpriteText();
|
||||
displayedCountText = CreateText();
|
||||
|
||||
UpdateDisplay();
|
||||
Child = displayedCountSpriteText;
|
||||
Child = DrawableCount = (Drawable)displayedCountText;
|
||||
}
|
||||
|
||||
protected void UpdateDisplay()
|
||||
{
|
||||
if (displayedCountSpriteText != null)
|
||||
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
|
||||
if (displayedCountText != null)
|
||||
displayedCountText.Text = FormatCount(DisplayedCount);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -160,6 +162,15 @@ namespace osu.Game.Graphics.UserInterface
|
||||
this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the text. Delegates to <see cref="CreateSpriteText"/> by default.
|
||||
/// </summary>
|
||||
protected virtual IHasText CreateText() => CreateSpriteText();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="OsuSpriteText"/> which may be used to display this counter's text.
|
||||
/// May not be called if <see cref="CreateText"/> is overridden.
|
||||
/// </summary>
|
||||
protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Numeric.With(size: 40f),
|
||||
|
@ -7,6 +7,8 @@ using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -16,6 +18,14 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
public abstract class DifficultyCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// The beatmap for which difficulty will be calculated.
|
||||
/// </summary>
|
||||
protected IBeatmap Beatmap { get; private set; }
|
||||
|
||||
private Mod[] playableMods;
|
||||
private double clockRate;
|
||||
|
||||
private readonly Ruleset ruleset;
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
|
||||
@ -32,14 +42,45 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
||||
public DifficultyAttributes Calculate(params Mod[] mods)
|
||||
{
|
||||
mods = mods.Select(m => m.DeepClone()).ToArray();
|
||||
preProcess(mods);
|
||||
|
||||
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||
var skills = CreateSkills(Beatmap, playableMods, clockRate);
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
if (!Beatmap.HitObjects.Any())
|
||||
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
|
||||
|
||||
return calculate(playableBeatmap, mods, track.Rate);
|
||||
foreach (var hitObject in getDifficultyHitObjects())
|
||||
{
|
||||
foreach (var skill in skills)
|
||||
skill.ProcessInternal(hitObject);
|
||||
}
|
||||
|
||||
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
|
||||
}
|
||||
|
||||
public List<TimedDifficultyAttributes> CalculateTimed(params Mod[] mods)
|
||||
{
|
||||
preProcess(mods);
|
||||
|
||||
var attribs = new List<TimedDifficultyAttributes>();
|
||||
|
||||
if (!Beatmap.HitObjects.Any())
|
||||
return attribs;
|
||||
|
||||
var skills = CreateSkills(Beatmap, playableMods, clockRate);
|
||||
var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
|
||||
|
||||
foreach (var hitObject in getDifficultyHitObjects())
|
||||
{
|
||||
progressiveBeatmap.HitObjects.Add(hitObject.BaseObject);
|
||||
|
||||
foreach (var skill in skills)
|
||||
skill.ProcessInternal(hitObject);
|
||||
|
||||
attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
|
||||
}
|
||||
|
||||
return attribs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -57,24 +98,23 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="DifficultyHitObject"/>s to calculate against.
|
||||
/// </summary>
|
||||
private IEnumerable<DifficultyHitObject> getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate));
|
||||
|
||||
/// <summary>
|
||||
/// Performs required tasks before every calculation.
|
||||
/// </summary>
|
||||
/// <param name="mods">The original list of <see cref="Mod"/>s.</param>
|
||||
private void preProcess(Mod[] mods)
|
||||
{
|
||||
var skills = CreateSkills(beatmap, mods, clockRate);
|
||||
playableMods = mods.Select(m => m.DeepClone()).ToArray();
|
||||
Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||
|
||||
if (!beatmap.HitObjects.Any())
|
||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||
|
||||
var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
|
||||
|
||||
foreach (var hitObject in difficultyHitObjects)
|
||||
{
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
skill.ProcessInternal(hitObject);
|
||||
}
|
||||
}
|
||||
|
||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||
var track = new TrackVirtual(10000);
|
||||
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -86,7 +126,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
=> input.OrderBy(h => h.BaseObject.StartTime);
|
||||
|
||||
/// <summary>
|
||||
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
|
||||
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
||||
/// </summary>
|
||||
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
||||
{
|
||||
@ -154,14 +194,15 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmap"/> difficulty.
|
||||
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
||||
/// </summary>
|
||||
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.</param>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.
|
||||
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
|
||||
/// <param name="skills">The skills which processed the beatmap.</param>
|
||||
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
||||
@ -178,10 +219,51 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <summary>
|
||||
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.
|
||||
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
|
||||
/// <param name="mods">Mods to calculate difficulty with.</param>
|
||||
/// <param name="clockRate">Clockrate to calculate difficulty with.</param>
|
||||
/// <returns>The <see cref="Skill"/>s.</returns>
|
||||
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate);
|
||||
|
||||
/// <summary>
|
||||
/// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time.
|
||||
/// </summary>
|
||||
private class ProgressiveCalculationBeatmap : IBeatmap
|
||||
{
|
||||
private readonly IBeatmap baseBeatmap;
|
||||
|
||||
public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap)
|
||||
{
|
||||
this.baseBeatmap = baseBeatmap;
|
||||
}
|
||||
|
||||
public readonly List<HitObject> HitObjects = new List<HitObject>();
|
||||
|
||||
IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
|
||||
|
||||
#region Delegated IBeatmap implementation
|
||||
|
||||
public BeatmapInfo BeatmapInfo
|
||||
{
|
||||
get => baseBeatmap.BeatmapInfo;
|
||||
set => baseBeatmap.BeatmapInfo = value;
|
||||
}
|
||||
|
||||
public ControlPointInfo ControlPointInfo
|
||||
{
|
||||
get => baseBeatmap.ControlPointInfo;
|
||||
set => baseBeatmap.ControlPointInfo = value;
|
||||
}
|
||||
|
||||
public BeatmapMetadata Metadata => baseBeatmap.Metadata;
|
||||
public List<BreakPeriod> Breaks => baseBeatmap.Breaks;
|
||||
public double TotalBreakTime => baseBeatmap.TotalBreakTime;
|
||||
public IEnumerable<BeatmapStatistic> GetStatistics() => baseBeatmap.GetStatistics();
|
||||
public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength();
|
||||
public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone());
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
Normal file
25
osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="DifficultyAttributes"/> object and adds a time value for which the attribute is valid.
|
||||
/// Output by <see cref="DifficultyCalculator.CalculateTimed"/>.
|
||||
/// </summary>
|
||||
public class TimedDifficultyAttributes : IComparable<TimedDifficultyAttributes>
|
||||
{
|
||||
public readonly double Time;
|
||||
public readonly DifficultyAttributes Attributes;
|
||||
|
||||
public TimedDifficultyAttributes(double time, DifficultyAttributes attributes)
|
||||
{
|
||||
Time = time;
|
||||
Attributes = attributes;
|
||||
}
|
||||
|
||||
public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time);
|
||||
}
|
||||
}
|
@ -18,6 +18,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public event Action<JudgementResult> NewJudgement;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a judgement is reverted, usually due to rewinding gameplay.
|
||||
/// </summary>
|
||||
public event Action<JudgementResult> JudgementReverted;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of hits that can be judged.
|
||||
/// </summary>
|
||||
@ -71,6 +76,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
JudgedHits--;
|
||||
|
||||
RevertResultInternal(result);
|
||||
|
||||
JudgementReverted?.Invoke(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,12 +1,14 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -30,7 +32,12 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// The mods applied to the gameplay.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Mod> Mods;
|
||||
public readonly IReadOnlyList<Mod> Mods;
|
||||
|
||||
/// <summary>
|
||||
/// The gameplay score.
|
||||
/// </summary>
|
||||
public readonly Score Score;
|
||||
|
||||
/// <summary>
|
||||
/// A bindable tracking the last judgement result applied to any hit object.
|
||||
@ -39,11 +46,12 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
|
||||
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod> mods)
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
Ruleset = ruleset;
|
||||
Mods = mods;
|
||||
Score = score ?? new Score();
|
||||
Mods = mods ?? ArraySegment<Mod>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
219
osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
Normal file
219
osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
Normal file
@ -0,0 +1,219 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class PerformancePointsCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
protected override bool IsRollingProportional => true;
|
||||
|
||||
protected override double RollingDuration => 1000;
|
||||
|
||||
private const float alpha_when_invalid = 0.3f;
|
||||
|
||||
[CanBeNull]
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ScoreProcessor scoreProcessor { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private GameplayState gameplayState { get; set; }
|
||||
|
||||
[CanBeNull]
|
||||
private List<TimedDifficultyAttributes> timedAttributes;
|
||||
|
||||
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
|
||||
|
||||
private JudgementResult lastJudgement;
|
||||
|
||||
public PerformancePointsCounter()
|
||||
{
|
||||
Current.Value = DisplayedCount = 0;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache)
|
||||
{
|
||||
Colour = colours.BlueLighter;
|
||||
|
||||
if (gameplayState != null)
|
||||
{
|
||||
var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap);
|
||||
difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token)
|
||||
.ContinueWith(r => Schedule(() =>
|
||||
{
|
||||
timedAttributes = r.Result;
|
||||
IsValid = true;
|
||||
if (lastJudgement != null)
|
||||
onJudgementChanged(lastJudgement);
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (scoreProcessor != null)
|
||||
{
|
||||
scoreProcessor.NewJudgement += onJudgementChanged;
|
||||
scoreProcessor.JudgementReverted += onJudgementChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private bool isValid;
|
||||
|
||||
protected bool IsValid
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == isValid)
|
||||
return;
|
||||
|
||||
isValid = value;
|
||||
DrawableCount.FadeTo(isValid ? 1 : alpha_when_invalid, 1000, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void onJudgementChanged(JudgementResult judgement)
|
||||
{
|
||||
lastJudgement = judgement;
|
||||
|
||||
var attrib = getAttributeAtTime(judgement);
|
||||
|
||||
if (gameplayState == null || attrib == null)
|
||||
{
|
||||
IsValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo);
|
||||
|
||||
Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero);
|
||||
IsValid = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private DifficultyAttributes getAttributeAtTime(JudgementResult judgement)
|
||||
{
|
||||
if (timedAttributes == null || timedAttributes.Count == 0)
|
||||
return null;
|
||||
|
||||
int attribIndex = timedAttributes.BinarySearch(new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null));
|
||||
if (attribIndex < 0)
|
||||
attribIndex = ~attribIndex - 1;
|
||||
|
||||
return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes;
|
||||
}
|
||||
|
||||
protected override LocalisableString FormatCount(int count) => count.ToString(@"D");
|
||||
|
||||
protected override IHasText CreateText() => new TextComponent
|
||||
{
|
||||
Alpha = alpha_when_invalid
|
||||
};
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (scoreProcessor != null)
|
||||
scoreProcessor.NewJudgement -= onJudgementChanged;
|
||||
|
||||
loadCancellationSource?.Cancel();
|
||||
}
|
||||
|
||||
private class TextComponent : CompositeDrawable, IHasText
|
||||
{
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => text.Text;
|
||||
set => text.Text = value;
|
||||
}
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
|
||||
public TextComponent()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Font = OsuFont.Numeric.With(size: 16)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = @"pp",
|
||||
Font = OsuFont.Numeric.With(size: 8),
|
||||
Padding = new MarginPadding { Bottom = 1.5f }, // align baseline better
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap.
|
||||
private class GameplayWorkingBeatmap : WorkingBeatmap
|
||||
{
|
||||
private readonly IBeatmap gameplayBeatmap;
|
||||
|
||||
public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap)
|
||||
: base(gameplayBeatmap.BeatmapInfo, null)
|
||||
{
|
||||
this.gameplayBeatmap = gameplayBeatmap;
|
||||
}
|
||||
|
||||
public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
|
||||
=> gameplayBeatmap;
|
||||
|
||||
protected override IBeatmap GetBeatmap() => gameplayBeatmap;
|
||||
|
||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||
|
||||
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||
|
||||
protected internal override ISkin GetSkin() => throw new NotImplementedException();
|
||||
|
||||
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -161,13 +161,6 @@ namespace osu.Game.Screens.Play
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
Score = CreateScore();
|
||||
|
||||
// ensure the score is in a consistent state with the current player.
|
||||
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
|
||||
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||
Score.ScoreInfo.Mods = Mods.Value.ToArray();
|
||||
|
||||
PrepareReplay();
|
||||
|
||||
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo);
|
||||
@ -225,7 +218,14 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
|
||||
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value));
|
||||
Score = CreateScore(playableBeatmap);
|
||||
|
||||
// ensure the score is in a consistent state with the current player.
|
||||
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
|
||||
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||
Score.ScoreInfo.Mods = Mods.Value.ToArray();
|
||||
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score));
|
||||
|
||||
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
|
||||
|
||||
@ -988,8 +988,9 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// Creates the player's <see cref="Scoring.Score"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmap"></param>
|
||||
/// <returns>The <see cref="Scoring.Score"/>.</returns>
|
||||
protected virtual Score CreateScore() => new Score
|
||||
protected virtual Score CreateScore(IBeatmap beatmap) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = api.LocalUser.Value },
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play
|
||||
DrawableRuleset?.SetReplayScore(Score);
|
||||
}
|
||||
|
||||
protected override Score CreateScore() => createScore(GameplayState.Beatmap, Mods.Value);
|
||||
protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value);
|
||||
|
||||
// Don't re-import replay scores as they're already present in the database.
|
||||
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Spectator;
|
||||
@ -79,7 +80,7 @@ namespace osu.Game.Screens.Play
|
||||
NonFrameStableSeek(score.Replay.Frames[0].Time);
|
||||
}
|
||||
|
||||
protected override Score CreateScore() => score;
|
||||
protected override Score CreateScore(IBeatmap beatmap) => score;
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score)
|
||||
=> new SpectatorResultsScreen(score);
|
||||
|
@ -68,6 +68,7 @@ namespace osu.Game.Skinning
|
||||
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
|
||||
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
|
||||
|
||||
if (score != null)
|
||||
{
|
||||
@ -81,6 +82,13 @@ namespace osu.Game.Skinning
|
||||
|
||||
score.Position = new Vector2(0, vertical_offset);
|
||||
|
||||
if (ppCounter != null)
|
||||
{
|
||||
ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4;
|
||||
ppCounter.Origin = Anchor.TopCentre;
|
||||
ppCounter.Anchor = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
if (accuracy != null)
|
||||
{
|
||||
accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5);
|
||||
@ -123,6 +131,7 @@ namespace osu.Game.Skinning
|
||||
new SongProgress(),
|
||||
new BarHitErrorMeter(),
|
||||
new BarHitErrorMeter(),
|
||||
new PerformancePointsCounter()
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user