diff --git a/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs b/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs index 3f1494be0..bf655fe7a 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs @@ -10,7 +10,9 @@ namespace osu.Game.Rulesets.Sentakki.Tests.Statistics [TestFixture] public partial class TestSceneJudgementChart : OsuTestScene { - private readonly List testevents = new List + protected override Ruleset CreateRuleset() => new SentakkiRuleset(); + + private List testevents = new List { // Tap new HitEvent(0, HitResult.Great, new Tap(), new Tap(), null), diff --git a/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_DiagonalLinePattern.fs b/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_DiagonalLinePattern.fs new file mode 100644 index 000000000..fcc7956c1 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Resources/Shaders/sh_DiagonalLinePattern.fs @@ -0,0 +1,12 @@ +#include "sh_Utils.h" +#include "sh_Masking.h" + +layout(location = 0) out vec4 o_Colour; +layout(location = 2) in mediump vec2 v_TexCoord; + +void main(void) +{ + float DistanceToLine = mod((v_TexCoord.x+v_TexCoord.y) / (v_TexRect[2] - v_TexRect[0]), 0.3); + bool pixelLit = DistanceToLine < 0.15; + o_Colour = getRoundedColor( pixelLit ? vec4(1,1,1,1) : vec4(0.5,0.5,0.5,0.8), v_TexCoord); +} diff --git a/osu.Game.Rulesets.Sentakki/Statistics/ChartEntry.cs b/osu.Game.Rulesets.Sentakki/Statistics/ChartEntry.cs new file mode 100644 index 000000000..2940e236a --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Statistics/ChartEntry.cs @@ -0,0 +1,327 @@ +using System.Collections.Generic; +using System.Linq; +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.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Sentakki.Statistics +{ + internal partial class ChartEntry : CompositeDrawable + { + private static readonly Color4 accent_color = Color4Extensions.FromHex("#66FFCC"); + + private static readonly Color4 background_color = Color4Extensions.FromHex("#202624"); + + private const double bar_fill_duration = 3000; + + private readonly string name; + + private readonly IReadOnlyList hitEvents; + public ChartEntry(string name, IReadOnlyList hitEvents) + { + this.name = name; + this.hitEvents = hitEvents; + } + + private SimpleStatsSegment simpleStats = null!; + private Drawable detailedStats = null!; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Origin = Anchor.TopCentre; + RelativeSizeAxes = Axes.Both; + Height = 1f / 6f; + Scale = new Vector2(1, 0); + + Masking = true; + BorderThickness = 2; + BorderColour = accent_color; + CornerRadius = 5; + CornerExponent = 2.5f; + + Alpha = hitEvents.Any() ? 1 : 0.8f; + Colour = !hitEvents.Any() ? Color4.DarkGray : Color4.White; + + bool allPerfect = hitEvents.Any() && hitEvents.All(h => h.Result == HitResult.Great); + + var bg = background_color; + + if (allPerfect) + bg = Interpolation.ValueAt(0.1, bg, accent_color, 0, 1); + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = bg, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new Dimension[]{ + new Dimension(GridSizeMode.Distributed, minSize: 10, maxSize:30), + new Dimension(GridSizeMode.AutoSize), // Left text + new Dimension(GridSizeMode.Distributed, minSize: 5, maxSize: 30), + new Dimension(GridSizeMode.Distributed), // Bars + new Dimension(GridSizeMode.Distributed, minSize: 5, maxSize: 30), + new Dimension(GridSizeMode.AutoSize), // Total count + new Dimension(GridSizeMode.AutoSize), // Detailed count + new Dimension(GridSizeMode.Distributed, minSize: 10, maxSize:30), + }, + Content = new[]{ + new Drawable[]{ + null!, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(102,20), + Child = new OsuSpriteText + { + Colour = accent_color, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = name.ToUpper(), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Bold), + }, + }, + null!, // Container only + simpleStats = new SimpleStatsSegment(hitEvents), + null!, + new ResultsCounter("Total", hitEvents.Count) + { + Colour = accent_color , + }, + detailedStats = new DetailedStatsSegment(hitEvents) { + Scale = new Vector2(0,1), + Margin = new MarginPadding{ Left = 15 } + }, + null!, + } + } + }, + }; + } + + protected override bool OnHover(HoverEvent e) + { + detailedStats.ScaleTo(Vector2.One, 200, Easing.OutElasticQuarter); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + detailedStats.ScaleTo(new Vector2(0, 1), 200, Easing.OutExpo); + } + + + public void AnimateEntry(double entryDuration) + { + this.ScaleTo(1, entryDuration, Easing.OutBack); + simpleStats.AnimateEntry(); + } + + private partial class SimpleStatsSegment : GridContainer + { + private Container ratioBoxes; + private IReadOnlyList hitEvents; + + public SimpleStatsSegment(IReadOnlyList hitEvents) + { + this.hitEvents = hitEvents; + RelativeSizeAxes = Axes.Both; + Content = new[]{ + new Drawable[]{ + new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + CornerRadius = 5, + CornerExponent = 2.5f, + Masking = true, + BorderThickness = 2, + BorderColour = accent_color, + Height = 0.8f, + Children = new Drawable[]{ + new Box + { + Alpha = 0, + AlwaysPresent = true, + RelativeSizeAxes = Axes.Both + }, + ratioBoxes = new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0,1), + } + } + }, + } + }; + + if (!hitEvents.Any()) return; + + addRatioBoxFor(HitResult.Ok); + addRatioBoxFor(HitResult.Good); + addRatioBoxFor(HitResult.Great); + } + + public void AnimateEntry() + { + ratioBoxes.ResizeWidthTo(1, bar_fill_duration, Easing.OutPow10); + } + + // This will add a box for each valid sentakki HitResult, excluding those that aren't visible + private void addRatioBoxFor(HitResult result) + { + int resultCount = hitEvents.Count(e => e.Result >= result); + + if (resultCount == 0) return; + + ratioBoxes.Add(new RatioBox + { + RelativeSizeAxes = Axes.Both, + Width = (float)resultCount / hitEvents.Count, + Colour = result.GetColorForSentakkiResult(), + Alpha = .8f + }); + } + } + + public partial class DetailedStatsSegment : FillFlowContainer + { + private static readonly HitResult[] valid_results = new HitResult[]{ + HitResult.Great, + HitResult.Good, + HitResult.Ok, + HitResult.Miss + }; + + public DetailedStatsSegment(IReadOnlyList hitEvents) + { + Anchor = Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + Spacing = new Vector2(10f, 0f); + Direction = FillDirection.Horizontal; + + foreach (var resultType in valid_results) + { + int amount = hitEvents.Count(e => e.Result == resultType); + var colour = resultType.GetColorForSentakkiResult(); + var hspa = new HSPAColour(colour) { P = 0.6f }.ToColor4(); + AddInternal(new ResultsCounter(resultType.GetDisplayNameForSentakkiResult(), amount) + { + Colour = Interpolation.ValueAt(0.5f, colour, hspa, 0, 1), + Scale = new Vector2(0.8f) + }); + } + } + } + + private partial class ResultsCounter : FillFlowContainer + { + public ResultsCounter(string title, int count) + { + AutoSizeAxes = Axes.Both; + Spacing = new Vector2(0, -5); + Direction = FillDirection.Vertical; + Anchor = Origin = Anchor.Centre; + + AddRangeInternal(new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = title.ToUpper(), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Bold) + }, + new TotalNoteCounter(count){ + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + } + } + + public partial class TotalNoteCounter : RollingCounter + { + protected override double RollingDuration => bar_fill_duration; + + protected override Easing RollingEasing => Easing.OutPow10; + + protected override LocalisableString FormatCount(int count) => count.ToString("N0"); + + private int totalValue; + + public TotalNoteCounter(int value) + { + Current = new Bindable { Value = 0 }; + totalValue = value; + } + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + this.TransformBindableTo(Current, totalValue); + } + } + + private partial class RatioBox : Sprite, ITexturedShaderDrawable + { + public new IShader TextureShader { get; private set; } = null!; + + protected override DrawNode CreateDrawNode() => new RatioBoxDrawNode(this); + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders, IRenderer renderer) + { + Texture = renderer.WhitePixel.Crop(new Framework.Graphics.Primitives.RectangleF(0, 0, 1f, 1f), Axes.None, WrapMode.Repeat, WrapMode.Repeat); + TextureRelativeSizeAxes = Axes.None; + TextureRectangle = new Framework.Graphics.Primitives.RectangleF(0, 0, 50, 50); + + try + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "DiagonalLinePattern"); + } + catch // Fallback + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); + } + } + + private class RatioBoxDrawNode : SpriteDrawNode + { + public RatioBoxDrawNode(Sprite source) : base(source) { } + protected override bool CanDrawOpaqueInterior => false; + } + } + } +} diff --git a/osu.Game.Rulesets.Sentakki/Statistics/JudgementChart.cs b/osu.Game.Rulesets.Sentakki/Statistics/JudgementChart.cs index 65cebb209..4c9683af2 100644 --- a/osu.Game.Rulesets.Sentakki/Statistics/JudgementChart.cs +++ b/osu.Game.Rulesets.Sentakki/Statistics/JudgementChart.cs @@ -1,263 +1,58 @@ +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Sentakki.Objects; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Sentakki.Statistics { - public partial class JudgementChart : CompositeDrawable + public partial class JudgementChart : FillFlowContainer { private const double entry_animation_duration = 150; - private const double bar_fill_duration = 3000; - public JudgementChart(List hitEvents) + // The list of entries that we should create, placed here to reduce dupe code + private static readonly (string, Func)[] entries = { - Origin = Anchor.Centre; - Anchor = Anchor.Centre; - Size = new Vector2(500, 250); - AddRangeInternal(new Drawable[] - { - new NoteEntry - { - ObjectName = "Tap", - HitEvents = hitEvents.Where(e => e.HitObject is Tap x && !x.Break).ToList(), - Position = new Vector2(0, 0), - InitialLifetimeOffset = entry_animation_duration * 0 - }, - new NoteEntry - { - ObjectName = "Hold", - HitEvents = hitEvents.Where(e => (e.HitObject is Hold or Hold.HoldHead) && !((SentakkiLanedHitObject)e.HitObject).Break).ToList(), - Position = new Vector2(0, .16f), - InitialLifetimeOffset = entry_animation_duration * 1 - }, - new NoteEntry - { - ObjectName = "Slide", - HitEvents = hitEvents.Where(e => e.HitObject is SlideBody).ToList(), - Position = new Vector2(0, .32f), - InitialLifetimeOffset = entry_animation_duration * 2 - }, - new NoteEntry - { - ObjectName = "Touch", - HitEvents = hitEvents.Where(e => e.HitObject is Touch).ToList(), - Position = new Vector2(0, .48f), - InitialLifetimeOffset = entry_animation_duration * 3 - }, - new NoteEntry - { - ObjectName = "Touch Hold", - HitEvents = hitEvents.Where(e => e.HitObject is TouchHold).ToList(), - Position = new Vector2(0, .64f), - InitialLifetimeOffset = entry_animation_duration * 4 - }, - new NoteEntry - { - ObjectName = "Break", - HitEvents = hitEvents.Where(e => e.HitObject is SentakkiLanedHitObject x && x.Break).ToList(), - Position = new Vector2(0, .80f), - InitialLifetimeOffset = entry_animation_duration * 5 - }, - }); - } - - public partial class NoteEntry : Container - { - public double InitialLifetimeOffset; - private Container progressBox = null!; - private RollingCounter noteCounter = null!; - - public string ObjectName = "Object"; - public List HitEvents = null!; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - float goodCount = 0; - float greatCount = 0; - float perfectCount = 0; - - foreach (var e in HitEvents) - { - switch (e.Result) - { - case HitResult.Great: - ++perfectCount; - goto case HitResult.Good; - - case HitResult.Good: - ++greatCount; - goto case HitResult.Ok; - - case HitResult.Ok: - ++goodCount; - break; - } - } - - Color4 textColour = !HitEvents.Any() ? Color4Extensions.FromHex("bcbcbc") : (perfectCount == HitEvents.Count) ? Color4.White : Color4Extensions.FromHex("#3c5394"); - Color4 boxColour = !HitEvents.Any() ? Color4Extensions.FromHex("808080") : (perfectCount == HitEvents.Count) ? Color4Extensions.FromHex("fda908") : Color4Extensions.FromHex("#DCE9F9"); - Color4 borderColour = !HitEvents.Any() ? Color4Extensions.FromHex("536277") : - (perfectCount == HitEvents.Count) ? Color4Extensions.FromHex("fda908") : Color4Extensions.FromHex("#98b8df"); - Color4 numberColour = (perfectCount == HitEvents.Count && HitEvents.Any()) ? Color4.White : Color4Extensions.FromHex("#3c5394"); - - Anchor = Anchor.TopCentre; - Origin = Anchor.TopCentre; - RelativePositionAxes = Axes.Both; - RelativeSizeAxes = Axes.Both; - Size = new Vector2(1, .16f); - Scale = new Vector2(1, 0); - Alpha = 0; - Masking = true; - BorderThickness = 2; - BorderColour = borderColour; - CornerRadius = 5; - CornerExponent = 2.5f; - AlwaysPresent = true; - - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = boxColour, - }, - new Container - { - // Left - RelativeSizeAxes = Axes.Both, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Size = new Vector2(.33f, 1), - Child = new OsuSpriteText - { - Colour = textColour, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = ObjectName.ToUpper(), - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Bold) - } - }, - progressBox = new Container - { - // Centre - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(.34f, .8f), - CornerRadius = 5, - CornerExponent = 2.5f, - Masking = true, - BorderThickness = 2, - BorderColour = Color4.Black, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = !HitEvents.Any() ? Color4Extensions.FromHex("343434") : Color4.DarkGray, - } - } - }, - new Container - { - // Right - RelativeSizeAxes = Axes.Both, - Origin = Anchor.CentreRight, - Anchor = Anchor.CentreRight, - Size = new Vector2(.33f, 1), - Child = noteCounter = new TotalNoteCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = numberColour, - Current = { Value = 0 }, - } - }, - }; + ("Tap", e => e.HitObject is Tap x && !x.Break), + ("Hold", e => (e.HitObject is Hold or Hold.HoldHead) && !((SentakkiLanedHitObject)e.HitObject).Break), + ("Slide", e => e.HitObject is SlideBody x), + ("Touch", e => e.HitObject is Touch), + ("Touch Hold", e => e.HitObject is TouchHold), + ("Break", e => e.HitObject is SentakkiLanedHitObject x && x.Break), + }; - progressBox.AddRange(new Drawable[] - { - new ChartBar(HitResult.Ok, goodCount / HitEvents.Count) - { - InitialLifetimeOffset = InitialLifetimeOffset - }, - new ChartBar(HitResult.Good, greatCount / HitEvents.Count) - { - InitialLifetimeOffset = InitialLifetimeOffset - }, - new ChartBar(HitResult.Great, perfectCount / HitEvents.Count) - { - InitialLifetimeOffset = InitialLifetimeOffset - }, - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - ScheduleAfterChildren(() => - { - using (BeginDelayedSequence(InitialLifetimeOffset)) - { - this.ScaleTo(1, entry_animation_duration, Easing.OutBack).FadeIn(); - noteCounter.Current.Value = HitEvents.Count; - } - }); - } + private readonly IReadOnlyList hitEvents; - public partial class TotalNoteCounter : RollingCounter - { - protected override double RollingDuration => bar_fill_duration; + public JudgementChart(IReadOnlyList hitEvents) + { + this.hitEvents = hitEvents; + } - protected override Easing RollingEasing => Easing.OutPow10; + [BackgroundDependencyLoader] + private void load() + { + Anchor = Origin = Anchor.Centre; + Size = new Vector2(1, 250); + RelativeSizeAxes = Axes.X; - protected override LocalisableString FormatCount(long count) => count.ToString("N0"); + foreach (var (name, predicate) in entries) + AddInternal(new ChartEntry(name, hitEvents.Where(predicate).ToList())); + } - protected override OsuSpriteText CreateSpriteText() - { - return new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), - }; - } - } + protected override void LoadComplete() + { + base.LoadComplete(); - private partial class ChartBar : Container + double delay = 0; + foreach (ChartEntry child in Children) { - public double InitialLifetimeOffset; - - public ChartBar(HitResult result, float progress) - { - RelativeSizeAxes = Axes.Both; - Size = new Vector2(float.IsNaN(progress) ? 0 : progress, 1); - Scale = new Vector2(0, 1); - Add(new Box - { - RelativeSizeAxes = Axes.Both, - Colour = result.GetColorForSentakkiResult() - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - this.Delay(InitialLifetimeOffset).ScaleTo(1, bar_fill_duration, Easing.OutPow10); - } + using (BeginDelayedSequence(delay, true)) + child.AnimateEntry(entry_animation_duration); + delay += entry_animation_duration; } } }