diff --git a/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs b/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs index ae0f2066d..3ee141ea6 100644 --- a/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs +++ b/osu.Game.Rulesets.Sentakki.Tests/Statistics/TestSceneJudgementChart.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Sentakki.Objects; using osu.Game.Rulesets.Sentakki.Statistics; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Sentakki.Tests.Statistics { @@ -58,7 +60,13 @@ public partial class TestSceneJudgementChart : OsuTestScene public TestSceneJudgementChart() { - Add(new JudgementChart(testevents)); + Add(new JudgementChart(testevents) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, 250), + RelativeSizeAxes = Axes.X, + }); } } } diff --git a/osu.Game.Rulesets.Sentakki/Statistics/ChartEntry.cs b/osu.Game.Rulesets.Sentakki/Statistics/ChartEntry.cs deleted file mode 100644 index b26a27650..000000000 --- a/osu.Game.Rulesets.Sentakki/Statistics/ChartEntry.cs +++ /dev/null @@ -1,329 +0,0 @@ -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); - addRatioBoxFor(HitResult.Perfect); - } - - 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.Perfect, - 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 53ef07607..2412d9f67 100644 --- a/osu.Game.Rulesets.Sentakki/Statistics/JudgementChart.cs +++ b/osu.Game.Rulesets.Sentakki/Statistics/JudgementChart.cs @@ -1,22 +1,26 @@ using System; 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.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 +namespace osu.Game.Rulesets.Sentakki.Statistics; + +public partial class JudgementChart : TableContainer { - public partial class JudgementChart : FillFlowContainer - { - private const double entry_animation_duration = 150; + private static readonly Color4 accent_color = Color4Extensions.FromHex("#66FFCC"); - // The list of entries that we should create, placed here to reduce dupe code - private static readonly (string, Func)[] entries = - { + private static readonly (string, Func)[] hitObjectTypes = + { ("Tap", e => e.HitObject is Tap x && !x.Break), ("Hold", e => ((e.HitObject is Hold.HoldHead) && !((SentakkiLanedHitObject)e.HitObject).Break) || e.HitObject is Hold), ("Slide", e => e.HitObject is SlideBody x), @@ -24,37 +28,139 @@ private static readonly (string, Func)[] entries = ("Touch Hold", e => e.HitObject is TouchHold), // Note Hold and Slide breaks are applied to child objects, not itself. ("Break", e => e.HitObject is SentakkiLanedHitObject x && (x is not Hold) && (x is not Slide) && x.Break), - }; + }; + + private static readonly HitResult[] valid_results = new HitResult[]{ + HitResult.Perfect, + HitResult.Great, + HitResult.Good, + HitResult.Ok, + HitResult.Miss + }; - private readonly IReadOnlyList hitEvents; + public JudgementChart(IReadOnlyList hitEvents) + { + var columns = new TableColumn[7]; + Array.Fill(columns, new TableColumn(null, Anchor.Centre, new Dimension(GridSizeMode.Distributed))); + Columns = columns; + + RowSize = new Dimension(GridSizeMode.Distributed); + + var content = new Drawable[6, 7]; - public JudgementChart(IReadOnlyList hitEvents) + for (int i = 0; i < hitObjectTypes.Length; ++i) { - this.hitEvents = hitEvents; + var entry = hitObjectTypes[i]; + + Dictionary results = collectHitResultsFor(hitEvents.Where(entry.Item2)); + + int sum = results.Sum(kvp => kvp.Value); + + bool perfected = sum == results.GetValueOrDefault(HitResult.Perfect) + results.GetValueOrDefault(HitResult.Great); + bool critPerfect = sum == results.GetValueOrDefault(HitResult.Perfect); + + Color4 specialColor = Color4.White; + + if (critPerfect) + specialColor = HitResult.Perfect.GetColorForSentakkiResult(); + else if (perfected) + specialColor = HitResult.Great.GetColorForSentakkiResult(); + + + // The alpha will be used to "disable" an hitobject entry if they don't exist + float commonAlpha = sum == 0 ? 0.1f : 1; + + content[i, 0] = new OsuSpriteText() + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Bold), + Text = entry.Item1, + Colour = specialColor, + Alpha = commonAlpha + }; + + // Total notes + content[i, 1] = new TotalNoteCounter(sum, true) + { + Colour = accent_color, + Alpha = commonAlpha + }; + + for (int j = 0; j < valid_results.Length; ++j) + { + content[i, 2 + j] = new TotalNoteCounter(results.GetValueOrDefault(valid_results[j])) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = valid_results[j].GetColorForSentakkiResult(), + Alpha = commonAlpha + }; + } } - [BackgroundDependencyLoader] - private void load() + Content = content; + } + + protected override Drawable CreateHeader(int index, TableColumn? column) + { + if (index == 0) + return null!; + + string text = index == 1 ? "TOTAL" : valid_results[index - 2].GetDisplayNameForSentakkiResult(); + var Colour = index == 1 ? accent_color : valid_results[index - 2].GetColorForSentakkiResult(); + + return new OsuSpriteText() { - Anchor = Origin = Anchor.Centre; - Size = new Vector2(1, 250); - RelativeSizeAxes = Axes.X; + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Bold), + Text = text, + Colour = Colour + }; + } + + private static Dictionary collectHitResultsFor(IEnumerable hitEvents) + { + var resultGroups = hitEvents.GroupBy(h => h.Result); + + Dictionary counts = new(); + + foreach (var he in resultGroups) + counts[he.Key] = he.Count(); + + return counts; + } - foreach (var (name, predicate) in entries) - AddInternal(new ChartEntry(name, hitEvents.Where(predicate).ToList())); + private partial class TotalNoteCounter : RollingCounter + { + protected override double RollingDuration => 3000; + + protected override Easing RollingEasing => Easing.OutPow10; + + protected override LocalisableString FormatCount(int count) => count.ToString("N0"); + + private readonly int totalValue; + private readonly bool bold; + + public TotalNoteCounter(int value, bool bold = false) + { + Current = new Bindable { Value = 0 }; + totalValue = value; + this.bold = bold; } + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 20, weight: bold ? FontWeight.Bold : FontWeight.Regular), + }; + protected override void LoadComplete() { base.LoadComplete(); - - double delay = 0; - foreach (ChartEntry child in Children) - { - using (BeginDelayedSequence(delay, true)) - child.AnimateEntry(entry_animation_duration); - delay += entry_animation_duration; - } + this.TransformBindableTo(Current, totalValue); } } }