diff --git a/osu.Game.Mode.Osu/Objects/BezierApproximator.cs b/osu.Game.Mode.Osu/Objects/BezierApproximator.cs new file mode 100644 index 0000000000..f08b7aa377 --- /dev/null +++ b/osu.Game.Mode.Osu/Objects/BezierApproximator.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using OpenTK; + +namespace osu.Game.Modes.Osu.Objects +{ + public class BezierApproximator + { + private int count; + private List controlPoints; + private Vector2[] subdivisionBuffer1; + private Vector2[] subdivisionBuffer2; + + private const float TOLERANCE = 0.5f; + private const float TOLERANCE_SQ = TOLERANCE * TOLERANCE; + + public BezierApproximator(List controlPoints) + { + this.controlPoints = controlPoints; + count = controlPoints.Count; + + subdivisionBuffer1 = new Vector2[count]; + subdivisionBuffer2 = new Vector2[count * 2 - 1]; + } + + /// + /// Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds. + /// NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function + /// checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts + /// need to have a denser approximation to be more "flat". + /// + /// The control points to check for flatness. + /// Whether the control points are flat enough. + private static bool IsFlatEnough(Vector2[] controlPoints) + { + for (int i = 1; i < controlPoints.Length - 1; i++) + if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > TOLERANCE_SQ) + return false; + + return true; + } + + /// + /// Subdivides n control points representing a bezier curve into 2 sets of n control points, each + /// describing a bezier curve equivalent to a half of the original curve. Effectively this splits + /// the original curve into 2 curves which result in the original curve when pieced back together. + /// + /// The control points to split. + /// Output: The control points corresponding to the left half of the curve. + /// Output: The control points corresponding to the right half of the curve. + private void Subdivide(Vector2[] controlPoints, Vector2[] l, Vector2[] r) + { + Vector2[] midpoints = subdivisionBuffer1; + + for (int i = 0; i < count; ++i) + midpoints[i] = controlPoints[i]; + + for (int i = 0; i < count; i++) + { + l[i] = midpoints[0]; + r[count - i - 1] = midpoints[count - i - 1]; + + for (int j = 0; j < count - i - 1; j++) + midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2; + } + } + + /// + /// This uses De Casteljau's algorithm to obtain an optimal + /// piecewise-linear approximation of the bezier curve with the same amount of points as there are control points. + /// + /// The control points describing the bezier curve to be approximated. + /// The points representing the resulting piecewise-linear approximation. + private void Approximate(Vector2[] controlPoints, List output) + { + Vector2[] l = subdivisionBuffer2; + Vector2[] r = subdivisionBuffer1; + + Subdivide(controlPoints, l, r); + + for (int i = 0; i < count - 1; ++i) + l[count + i] = r[i + 1]; + + output.Add(controlPoints[0]); + for (int i = 1; i < count - 1; ++i) + { + int index = 2 * i; + Vector2 p = 0.25f * (l[index - 1] + 2 * l[index] + l[index + 1]); + output.Add(p); + } + } + + /// + /// Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing + /// the control points until their approximation error vanishes below a given threshold. + /// + /// The control points describing the curve. + /// A list of vectors representing the piecewise-linear approximation. + public List CreateBezier() + { + List output = new List(); + + if (count == 0) + return output; + + Stack toFlatten = new Stack(); + Stack freeBuffers = new Stack(); + + // "toFlatten" contains all the curves which are not yet approximated well enough. + // We use a stack to emulate recursion without the risk of running into a stack overflow. + // (More specifically, we iteratively and adaptively refine our curve with a + // Depth-first search + // over the tree resulting from the subdivisions we make.) + toFlatten.Push(controlPoints.ToArray()); + + Vector2[] leftChild = subdivisionBuffer2; + + while (toFlatten.Count > 0) + { + Vector2[] parent = toFlatten.Pop(); + if (IsFlatEnough(parent)) + { + // If the control points we currently operate on are sufficiently "flat", we use + // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation + // of the bezier curve represented by our control points, consisting of the same amount + // of points as there are control points. + Approximate(parent, output); + freeBuffers.Push(parent); + continue; + } + + // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep + // subdividing the curve we are currently operating on. + Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[count]; + Subdivide(parent, leftChild, rightChild); + + // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration. + for (int i = 0; i < count; ++i) + parent[i] = leftChild[i]; + + toFlatten.Push(rightChild); + toFlatten.Push(parent); + } + + output.Add(controlPoints[count - 1]); + return output; + } + } +} \ No newline at end of file diff --git a/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 0e3a03665c..80e1f2bb7f 100644 --- a/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -74,7 +74,5 @@ namespace osu.Game.Modes.Osu.Objects.Drawables Hit100, [Description(@"300")] Hit300, - [Description(@"500")] - Hit500 } } diff --git a/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs index f9fc866414..8882049cbb 100644 --- a/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs @@ -75,12 +75,24 @@ namespace osu.Game.Modes.Osu.Objects.Drawables ball.Position = slider.Curve.PositionAt(t); } + protected override void CheckJudgement(bool userTriggered) + { + var j = Judgement as OsuJudgementInfo; + var sc = startCircle.Judgement as OsuJudgementInfo; + + if (!userTriggered && Time.Current >= HitObject.EndTime) + { + j.Score = sc.Score; + j.Result = sc.Result; + } + } + protected override void UpdateState(ArmedState state) { base.UpdateState(state); Delay(HitObject.Duration); - FadeOut(100); + FadeOut(300); } private class Ball : Container diff --git a/osu.Game.Mode.Osu/Objects/Slider.cs b/osu.Game.Mode.Osu/Objects/Slider.cs index 7ee131659a..a0cdbeae7c 100644 --- a/osu.Game.Mode.Osu/Objects/Slider.cs +++ b/osu.Game.Mode.Osu/Objects/Slider.cs @@ -1,9 +1,7 @@ //Copyright (c) 2007-2016 ppy Pty Ltd . //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using osu.Game.Database; -using OpenTK; using osu.Game.Beatmaps; using System; @@ -25,188 +23,6 @@ namespace osu.Game.Modes.Osu.Objects public SliderCurve Curve; } - public class SliderCurve - { - public double Length; - - public List Path; - - public CurveTypes CurveType; - - private List calculatedPath; - - public void Calculate() - { - switch (CurveType) - { - case CurveTypes.Linear: - calculatedPath = Path; - break; - default: - var bezier = new BezierApproximator(Path); - calculatedPath = bezier.CreateBezier(); - break; - } - } - - public Vector2 PositionAt(double progress) - { - progress = MathHelper.Clamp(progress, 0, 1); - - double index = progress * (calculatedPath.Count - 1); - int flooredIndex = (int)index; - - Vector2 pos = calculatedPath[flooredIndex]; - if (index != flooredIndex) - pos += (calculatedPath[flooredIndex + 1] - pos) * (float)(index - flooredIndex); - - return pos; - } - } - - public class BezierApproximator - { - private int count; - private List controlPoints; - private Vector2[] subdivisionBuffer1; - private Vector2[] subdivisionBuffer2; - - private const float TOLERANCE = 0.5f; - private const float TOLERANCE_SQ = TOLERANCE * TOLERANCE; - - public BezierApproximator(List controlPoints) - { - this.controlPoints = controlPoints; - count = controlPoints.Count; - - subdivisionBuffer1 = new Vector2[count]; - subdivisionBuffer2 = new Vector2[count * 2 - 1]; - } - - /// - /// Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds. - /// NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function - /// checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts - /// need to have a denser approximation to be more "flat". - /// - /// The control points to check for flatness. - /// Whether the control points are flat enough. - private static bool IsFlatEnough(Vector2[] controlPoints) - { - for (int i = 1; i < controlPoints.Length - 1; i++) - if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > TOLERANCE_SQ) - return false; - - return true; - } - - /// - /// Subdivides n control points representing a bezier curve into 2 sets of n control points, each - /// describing a bezier curve equivalent to a half of the original curve. Effectively this splits - /// the original curve into 2 curves which result in the original curve when pieced back together. - /// - /// The control points to split. - /// Output: The control points corresponding to the left half of the curve. - /// Output: The control points corresponding to the right half of the curve. - private void Subdivide(Vector2[] controlPoints, Vector2[] l, Vector2[] r) - { - Vector2[] midpoints = subdivisionBuffer1; - - for (int i = 0; i < count; ++i) - midpoints[i] = controlPoints[i]; - - for (int i = 0; i < count; i++) - { - l[i] = midpoints[0]; - r[count - i - 1] = midpoints[count - i - 1]; - - for (int j = 0; j < count - i - 1; j++) - midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2; - } - } - - /// - /// This uses De Casteljau's algorithm to obtain an optimal - /// piecewise-linear approximation of the bezier curve with the same amount of points as there are control points. - /// - /// The control points describing the bezier curve to be approximated. - /// The points representing the resulting piecewise-linear approximation. - private void Approximate(Vector2[] controlPoints, List output) - { - Vector2[] l = subdivisionBuffer2; - Vector2[] r = subdivisionBuffer1; - - Subdivide(controlPoints, l, r); - - for (int i = 0; i < count - 1; ++i) - l[count + i] = r[i + 1]; - - output.Add(controlPoints[0]); - for (int i = 1; i < count - 1; ++i) - { - int index = 2 * i; - Vector2 p = 0.25f * (l[index - 1] + 2 * l[index] + l[index + 1]); - output.Add(p); - } - } - - /// - /// Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing - /// the control points until their approximation error vanishes below a given threshold. - /// - /// The control points describing the curve. - /// A list of vectors representing the piecewise-linear approximation. - public List CreateBezier() - { - List output = new List(); - - if (count == 0) - return output; - - Stack toFlatten = new Stack(); - Stack freeBuffers = new Stack(); - - // "toFlatten" contains all the curves which are not yet approximated well enough. - // We use a stack to emulate recursion without the risk of running into a stack overflow. - // (More specifically, we iteratively and adaptively refine our curve with a - // Depth-first search - // over the tree resulting from the subdivisions we make.) - toFlatten.Push(controlPoints.ToArray()); - - Vector2[] leftChild = subdivisionBuffer2; - - while (toFlatten.Count > 0) - { - Vector2[] parent = toFlatten.Pop(); - if (IsFlatEnough(parent)) - { - // If the control points we currently operate on are sufficiently "flat", we use - // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation - // of the bezier curve represented by our control points, consisting of the same amount - // of points as there are control points. - Approximate(parent, output); - freeBuffers.Push(parent); - continue; - } - - // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep - // subdividing the curve we are currently operating on. - Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[count]; - Subdivide(parent, leftChild, rightChild); - - // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration. - for (int i = 0; i < count; ++i) - parent[i] = leftChild[i]; - - toFlatten.Push(rightChild); - toFlatten.Push(parent); - } - - output.Add(controlPoints[count - 1]); - return output; - } - } - public enum CurveTypes { Catmull, diff --git a/osu.Game.Mode.Osu/Objects/SliderCurve.cs b/osu.Game.Mode.Osu/Objects/SliderCurve.cs new file mode 100644 index 0000000000..4e691196f5 --- /dev/null +++ b/osu.Game.Mode.Osu/Objects/SliderCurve.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using OpenTK; + +namespace osu.Game.Modes.Osu.Objects +{ + public class SliderCurve + { + public double Length; + + public List Path; + + public CurveTypes CurveType; + + private List calculatedPath; + + private List calculateSubpath(List subpath) + { + switch (CurveType) + { + case CurveTypes.Linear: + return subpath; + default: + return new BezierApproximator(subpath).CreateBezier(); + } + } + + public void Calculate() + { + calculatedPath = new List(); + + // Sliders may consist of various subpaths separated by two consecutive vertices + // with the same position. The following loop parses these subpaths and computes + // their shape independently, consecutively appending them to calculatedPath. + List subpath = new List(); + for (int i = 0; i < Path.Count; ++i) + { + subpath.Add(Path[i]); + if (i == Path.Count - 1 || Path[i] == Path[i + 1]) + { + // If we already constructed a subpath previously, then the new subpath + // will have as starting position the end position of the previous subpath. + // Hence we can and should remove the previous endpoint to avoid a segment + // with 0 length. + if (calculatedPath.Count > 0) + calculatedPath.RemoveAt(calculatedPath.Count - 1); + + calculatedPath.AddRange(calculateSubpath(subpath)); + subpath.Clear(); + } + } + } + + public Vector2 PositionAt(double progress) + { + progress = MathHelper.Clamp(progress, 0, 1); + + double index = progress * (calculatedPath.Count - 1); + int flooredIndex = (int)index; + + Vector2 pos = calculatedPath[flooredIndex]; + if (index != flooredIndex) + pos += (calculatedPath[flooredIndex + 1] - pos) * (float)(index - flooredIndex); + + return pos; + } + } +} \ No newline at end of file diff --git a/osu.Game.Mode.Osu/OsuRuleset.cs b/osu.Game.Mode.Osu/OsuRuleset.cs index ac43e5501c..15799b134a 100644 --- a/osu.Game.Mode.Osu/OsuRuleset.cs +++ b/osu.Game.Mode.Osu/OsuRuleset.cs @@ -17,6 +17,8 @@ namespace osu.Game.Modes.Osu public override HitObjectParser CreateHitObjectParser() => new OsuHitObjectParser(); + public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + protected override PlayMode PlayMode => PlayMode.Osu; } } diff --git a/osu.Game.Mode.Osu/OsuScore.cs b/osu.Game.Mode.Osu/OsuScore.cs new file mode 100644 index 0000000000..5a70cea434 --- /dev/null +++ b/osu.Game.Mode.Osu/OsuScore.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Modes.Osu +{ + class OsuScore : Score + { + } +} diff --git a/osu.Game.Mode.Osu/OsuScoreProcessor.cs b/osu.Game.Mode.Osu/OsuScoreProcessor.cs new file mode 100644 index 0000000000..a5ebda6834 --- /dev/null +++ b/osu.Game.Mode.Osu/OsuScoreProcessor.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using osu.Game.Modes.Objects.Drawables; +using osu.Game.Modes.Osu.Objects.Drawables; + +namespace osu.Game.Modes.Osu +{ + class OsuScoreProcessor : ScoreProcessor + { + protected override void UpdateCalculations(JudgementInfo judgement) + { + if (judgement != null) + { + switch (judgement.Result) + { + case HitResult.Hit: + Combo.Value++; + break; + case HitResult.Miss: + Combo.Value = 0; + break; + } + } + + int score = 0; + int maxScore = 0; + + foreach (OsuJudgementInfo j in Judgements) + { + switch (j.Score) + { + case OsuScoreResult.Miss: + maxScore += 300; + break; + case OsuScoreResult.Hit50: + score += 50; + maxScore += 300; + break; + case OsuScoreResult.Hit100: + score += 100; + maxScore += 300; + break; + case OsuScoreResult.Hit300: + score += 300; + maxScore += 300; + break; + } + } + + TotalScore.Value = score; + Accuracy.Value = (double)score / maxScore; + } + } +} diff --git a/osu.Game.Mode.Osu/osu.Game.Modes.Osu.csproj b/osu.Game.Mode.Osu/osu.Game.Modes.Osu.csproj index 97b4ebcb99..ba6e714e0a 100644 --- a/osu.Game.Mode.Osu/osu.Game.Modes.Osu.csproj +++ b/osu.Game.Mode.Osu/osu.Game.Modes.Osu.csproj @@ -41,6 +41,7 @@ + @@ -53,6 +54,9 @@ + + + diff --git a/osu.Game.Modes.Catch/CatchRuleset.cs b/osu.Game.Modes.Catch/CatchRuleset.cs index 0195859cb7..eac762b4a0 100644 --- a/osu.Game.Modes.Catch/CatchRuleset.cs +++ b/osu.Game.Modes.Catch/CatchRuleset.cs @@ -18,6 +18,8 @@ namespace osu.Game.Modes.Catch protected override PlayMode PlayMode => PlayMode.Catch; + public override ScoreProcessor CreateScoreProcessor() => null; + public override HitObjectParser CreateHitObjectParser() => new OsuHitObjectParser(); } } diff --git a/osu.Game.Modes.Mania/ManiaRuleset.cs b/osu.Game.Modes.Mania/ManiaRuleset.cs index cb122084df..e91b2ed02e 100644 --- a/osu.Game.Modes.Mania/ManiaRuleset.cs +++ b/osu.Game.Modes.Mania/ManiaRuleset.cs @@ -19,6 +19,8 @@ namespace osu.Game.Modes.Mania protected override PlayMode PlayMode => PlayMode.Mania; + public override ScoreProcessor CreateScoreProcessor() => null; + public override HitObjectParser CreateHitObjectParser() => new OsuHitObjectParser(); } } diff --git a/osu.Game.Modes.Taiko/TaikoRuleset.cs b/osu.Game.Modes.Taiko/TaikoRuleset.cs index fa9d0862c7..e706387aaa 100644 --- a/osu.Game.Modes.Taiko/TaikoRuleset.cs +++ b/osu.Game.Modes.Taiko/TaikoRuleset.cs @@ -1,6 +1,7 @@ //Copyright (c) 2007-2016 ppy Pty Ltd . //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; @@ -18,6 +19,8 @@ namespace osu.Game.Modes.Taiko protected override PlayMode PlayMode => PlayMode.Taiko; + public override ScoreProcessor CreateScoreProcessor() => null; + public override HitObjectParser CreateHitObjectParser() => new OsuHitObjectParser(); } } diff --git a/osu.Game/Graphics/UserInterface/BackButton.cs b/osu.Game/Graphics/UserInterface/BackButton.cs index 5de8537c17..7ccc5c061b 100644 --- a/osu.Game/Graphics/UserInterface/BackButton.cs +++ b/osu.Game/Graphics/UserInterface/BackButton.cs @@ -19,6 +19,9 @@ namespace osu.Game.Graphics.UserInterface private Container leftContainer; private Container rightContainer; + private Box leftBox; + private Box rightBox; + private const double transform_time = 300.0; private const int pulse_length = 250; @@ -39,7 +42,7 @@ namespace osu.Game.Graphics.UserInterface Width = 0.4f, Children = new Drawable[] { - new Box + leftBox = new Box { RelativeSizeAxes = Axes.Both, Colour = new Color4(195, 40, 140, 255), @@ -61,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface Width = 0.6f, Children = new Drawable[] { - new Box + rightBox = new Box { Colour = new Color4(238, 51, 153, 255), Origin = Anchor.TopLeft, @@ -80,6 +83,12 @@ namespace osu.Game.Graphics.UserInterface } }; } + + public override bool Contains(Vector2 screenSpacePos) + { + return leftBox.Contains(screenSpacePos) || rightBox.Contains(screenSpacePos); + } + protected override bool OnHover(InputState state) { icon.ClearTransformations(); diff --git a/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs index a534c4b296..8e41fb6521 100644 --- a/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs @@ -13,13 +13,11 @@ namespace osu.Game.Modes.Objects.Drawables { public abstract class DrawableHitObject : Container, IStateful { - //todo: move to a more central implementation. this logic should not be at a drawable level. - public Action OnHit; - public Action OnMiss; + public event Action OnJudgement; public Container ChildObjects; - protected JudgementInfo Judgement; + public JudgementInfo Judgement; public abstract JudgementInfo CreateJudgementInfo(); @@ -73,20 +71,20 @@ namespace osu.Game.Modes.Objects.Drawables { default: State = ArmedState.Hit; - OnHit?.Invoke(this, Judgement); break; case HitResult.Miss: State = ArmedState.Miss; - OnMiss?.Invoke(this, Judgement); break; } + OnJudgement?.Invoke(this, Judgement); + return true; } protected virtual void CheckJudgement(bool userTriggered) { - + //todo: consider making abstract. } protected override void Update() @@ -113,6 +111,7 @@ namespace osu.Game.Modes.Objects.Drawables public class JudgementInfo { + public ulong? ComboAtHit; public HitResult? Result; public double TimeOffset; } diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index d35aab6568..b0304f6576 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -18,6 +18,8 @@ namespace osu.Game.Modes public abstract ScoreOverlay CreateScoreOverlay(); + public abstract ScoreProcessor CreateScoreProcessor(); + public abstract HitRenderer CreateHitRendererWith(List objects); public abstract HitObjectParser CreateHitObjectParser(); diff --git a/osu.Game/Modes/Score.cs b/osu.Game/Modes/Score.cs new file mode 100644 index 0000000000..ee90a9a0f9 --- /dev/null +++ b/osu.Game/Modes/Score.cs @@ -0,0 +1,19 @@ +//Copyright (c) 2007-2016 ppy Pty Ltd . +//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Game.Modes +{ + public class Score + { + public double TotalScore { get; set; } + public double Accuracy { get; set; } + public double Combo { get; set; } + public double MaxCombo { get; set; } + } +} diff --git a/osu.Game/Modes/ScoreProcesssor.cs b/osu.Game/Modes/ScoreProcesssor.cs new file mode 100644 index 0000000000..a34915eaef --- /dev/null +++ b/osu.Game/Modes/ScoreProcesssor.cs @@ -0,0 +1,54 @@ +//Copyright (c) 2007-2016 ppy Pty Ltd . +//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using osu.Framework.Configuration; +using osu.Game.Modes.Objects.Drawables; + +namespace osu.Game.Modes +{ + public abstract class ScoreProcessor + { + public virtual Score GetScore() => new Score() + { + TotalScore = TotalScore, + Combo = Combo, + MaxCombo = HighestCombo, + Accuracy = Accuracy + }; + + public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 }; + + public readonly BindableDouble Accuracy = new BindableDouble { MinValue = 0, MaxValue = 1 }; + + public readonly BindableInt Combo = new BindableInt(); + + public readonly BindableInt HighestCombo = new BindableInt(); + + public readonly List Judgements = new List(); + + public ScoreProcessor() + { + Combo.ValueChanged += delegate { HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); }; + } + + public void AddJudgement(JudgementInfo judgement) + { + Judgements.Add(judgement); + + UpdateCalculations(judgement); + + judgement.ComboAtHit = (ulong)Combo.Value; + } + + /// + /// Update any values that potentially need post-processing on a judgement change. + /// + /// A new JudgementInfo that triggered this calculation. May be null. + protected abstract void UpdateCalculations(JudgementInfo newJudgement); + } +} diff --git a/osu.Game/Modes/UI/ComboCounter.cs b/osu.Game/Modes/UI/ComboCounter.cs index 0d34d5dd9a..b150111387 100644 --- a/osu.Game/Modes/UI/ComboCounter.cs +++ b/osu.Game/Modes/UI/ComboCounter.cs @@ -262,5 +262,13 @@ namespace osu.Game.Modes.UI (d as ComboCounter).DisplayedCount = CurrentValue; } } + + public void Set(ulong value) + { + if (value == 0) + Roll(); + else + Count = value; + } } } diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index d37cf545a8..bd191a47a3 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -14,11 +14,21 @@ namespace osu.Game.Modes.UI { public abstract class HitRenderer : Container { - public Action OnHit; - public Action OnMiss; + public event Action OnJudgement; + + public event Action OnAllJudged; + + protected void TriggerOnJudgement(JudgementInfo j) + { + OnJudgement?.Invoke(j); + if (AllObjectsJudged) + OnAllJudged?.Invoke(); + } protected Playfield Playfield; + public bool AllObjectsJudged => Playfield.HitObjects.Children.First()?.Judgement.Result != null; //reverse depth sort means First() instead of Last(). + public IEnumerable DrawableObjects => Playfield.HitObjects.Children; } @@ -68,22 +78,13 @@ namespace osu.Game.Modes.UI if (drawableObject == null) continue; - drawableObject.OnHit = onHit; - drawableObject.OnMiss = onMiss; + drawableObject.OnJudgement += onJudgement; Playfield.Add(drawableObject); } } - private void onMiss(DrawableHitObject obj, JudgementInfo judgement) - { - OnMiss?.Invoke(obj.HitObject); - } - - private void onHit(DrawableHitObject obj, JudgementInfo judgement) - { - OnHit?.Invoke(obj.HitObject); - } + private void onJudgement(DrawableHitObject o, JudgementInfo j) => TriggerOnJudgement(j); protected abstract DrawableHitObject GetVisualRepresentation(T h); } diff --git a/osu.Game/Modes/UI/ScoreOverlay.cs b/osu.Game/Modes/UI/ScoreOverlay.cs index 2270533d47..c7441483f8 100644 --- a/osu.Game/Modes/UI/ScoreOverlay.cs +++ b/osu.Game/Modes/UI/ScoreOverlay.cs @@ -15,6 +15,7 @@ namespace osu.Game.Modes.UI public ComboCounter ComboCounter; public ScoreCounter ScoreCounter; public PercentageCounter AccuracyCounter; + public Score Score { get; set; } protected abstract KeyCounterCollection CreateKeyCounter(); protected abstract ComboCounter CreateComboCounter(); @@ -45,5 +46,13 @@ namespace osu.Game.Modes.UI AccuracyCounter = CreateAccuracyCounter(), }; } + + public void BindProcessor(ScoreProcessor processor) + { + //bind processor bindables to combocounter, score display etc. + processor.TotalScore.ValueChanged += delegate { ScoreCounter?.Set((ulong)processor.TotalScore.Value); }; + processor.Accuracy.ValueChanged += delegate { AccuracyCounter?.Set((float)processor.Accuracy.Value); }; + processor.Combo.ValueChanged += delegate { ComboCounter?.Set((ulong)processor.Combo.Value); }; + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 66cec7405e..2f35f1b5d5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -19,6 +19,8 @@ using OpenTK.Input; using MouseState = osu.Framework.Input.MouseState; using OpenTK; using osu.Framework.GameModes; +using osu.Game.Modes.UI; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { @@ -38,6 +40,9 @@ namespace osu.Game.Screens.Play private Ruleset ruleset; + private ScoreProcessor scoreProcessor; + private HitRenderer hitRenderer; + [BackgroundDependencyLoader] private void load(AudioManager audio, BeatmapDatabase beatmaps, OsuGameBase game) { @@ -84,10 +89,12 @@ namespace osu.Game.Screens.Play ruleset = Ruleset.GetRuleset(usablePlayMode); var scoreOverlay = ruleset.CreateScoreOverlay(); - var hitRenderer = ruleset.CreateHitRendererWith(beatmap.HitObjects); + scoreOverlay.BindProcessor(scoreProcessor = ruleset.CreateScoreProcessor()); - hitRenderer.OnHit += delegate (HitObject h) { scoreOverlay.OnHit(h); }; - hitRenderer.OnMiss += delegate (HitObject h) { scoreOverlay.OnMiss(h); }; + hitRenderer = ruleset.CreateHitRendererWith(beatmap.HitObjects); + + hitRenderer.OnJudgement += scoreProcessor.AddJudgement; + hitRenderer.OnAllJudged += hitRenderer_OnAllJudged; if (Autoplay) hitRenderer.Schedule(() => hitRenderer.DrawableObjects.ForEach(h => h.State = ArmedState.Hit)); @@ -106,6 +113,18 @@ namespace osu.Game.Screens.Play }; } + private void hitRenderer_OnAllJudged() + { + Delay(1000); + Schedule(delegate + { + Push(new Results + { + Score = scoreProcessor.GetScore() + }); + }); + } + protected override void OnEntering(GameMode last) { base.OnEntering(last); diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs index b1d55ab54a..a4219daf54 100644 --- a/osu.Game/Screens/Ranking/Results.cs +++ b/osu.Game/Screens/Ranking/Results.cs @@ -2,19 +2,29 @@ //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.GameModes; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transformations; +using osu.Game.Modes; using osu.Game.Screens.Backgrounds; +using OpenTK; using OpenTK.Graphics; namespace osu.Game.Screens.Ranking { - class Results : GameModeWhiteBox + class Results : OsuGameMode { - protected override BackgroundMode CreateBackground() => new BackgroundModeCustom(@"Backgrounds/bg4"); + protected override BackgroundMode CreateBackground() => new BackgroundModeBeatmap(Beatmap); + + private static readonly Vector2 BACKGROUND_BLUR = new Vector2(20); + + ScoreDisplay scoreDisplay; protected override void OnEntering(GameMode last) { base.OnEntering(last); - Background.Schedule(() => Background.FadeColour(Color4.DarkGray, 500)); + Background.Schedule(() => (Background as BackgroundModeBeatmap)?.BlurTo(BACKGROUND_BLUR, 1000)); } protected override bool OnExiting(GameMode next) @@ -22,5 +32,63 @@ namespace osu.Game.Screens.Ranking Background.Schedule(() => Background.FadeColour(Color4.White, 500)); return base.OnExiting(next); } + + public Score Score + { + set + { + scoreDisplay?.FadeOut(500); + scoreDisplay?.Expire(); + + scoreDisplay = new ScoreDisplay(value) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + Add(scoreDisplay); + + scoreDisplay.FadeIn(500); + scoreDisplay.ScaleTo(0.1f); + scoreDisplay.ScaleTo(1, 1000, EasingTypes.OutElastic); + scoreDisplay.RotateTo(360 * 5, 1000, EasingTypes.OutElastic); + + } + } + } + + class ScoreDisplay : Container + { + public ScoreDisplay(Score s) + { + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + new FlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FlowDirection.VerticalOnly, + Children = new Drawable[] + { + new SpriteText + { + TextSize = 40, + Text = $@"Accuracy: {s.Accuracy:#0.00%}", + }, + new SpriteText + { + TextSize = 40, + Text = $@"Score: {s.TotalScore}", + }, + new SpriteText + { + TextSize = 40, + Text = $@"MaxCombo: {s.MaxCombo}", + } + } + } + }; + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f1bdd47b68..f4d0749080 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -67,6 +67,8 @@ + +