diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Catch/CatchObjectInspectorRuleset.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Catch/CatchObjectInspectorRuleset.cs new file mode 100644 index 000000000..80b0c08cc --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Catch/CatchObjectInspectorRuleset.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; +using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI; +using osuTK.Input; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Catch +{ + public partial class CatchObjectInspectorRuleset : DrawableCatchEditorRuleset + { + private readonly CatchDifficultyHitObject[] difficultyHitObjects; + private CatchObjectInspectorPlayfield inspectorPlayfield; + + [Resolved] + private ObjectDifficultyValuesContainer difficultyValuesContainer { get; set; } + + public CatchObjectInspectorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods, ExtendedCatchDifficultyCalculator difficultyCalculator, double clockRate) + : base(ruleset, beatmap, mods) + { + difficultyHitObjects = difficultyCalculator.GetDifficultyHitObjects(beatmap, clockRate) + .Cast().ToArray(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inspectorPlayfield.SelectedObject.BindValueChanged(value => + { + difficultyValuesContainer.CurrentDifficultyHitObject.Value = difficultyHitObjects.FirstOrDefault(x => x.BaseObject.StartTime == value.NewValue?.HitObject.StartTime); + }); + } + + public override bool PropagatePositionalInputSubTree => true; + + public override bool PropagateNonPositionalInputSubTree => false; + + public override bool AllowBackwardsSeeks => true; + + protected override Playfield CreatePlayfield() => inspectorPlayfield = new CatchObjectInspectorPlayfield(Beatmap.Difficulty); + + private partial class CatchObjectInspectorPlayfield : CatchEditorPlayfield + { + public readonly Bindable SelectedObject = new Bindable(); + + public CatchObjectInspectorPlayfield(IBeatmapDifficultyInfo difficulty) + : base(difficulty) + { + DisplayJudgements.Value = false; + } + + protected override void OnHitObjectAdded(HitObject hitObject) + { + base.OnHitObjectAdded(hitObject); + + // Potential room for pooling here? + switch (hitObject) + { + case Fruit fruit: + { + HitObjectContainer.Add(new CatchSelectableHitObject(fruit) + { + PlayfieldSelectedObject = { BindTarget = SelectedObject } + }); + + break; + } + + case JuiceStream juiceStream: + { + foreach (var nested in juiceStream.NestedHitObjects) + { + if (nested is TinyDroplet) + continue; + + HitObjectContainer.Add(new CatchSelectableHitObject((CatchHitObject)nested) + { + PlayfieldSelectedObject = { BindTarget = SelectedObject } + }); + } + + break; + } + } + } + + protected override GameplayCursorContainer CreateCursor() => null!; + + protected override bool OnClick(ClickEvent e) + { + if (e.Button == MouseButton.Left) + { + SelectedObject.Value?.Deselect(); + SelectedObject.Value = null; + } + + return false; + } + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Catch/CatchSelectableHitObject.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Catch/CatchSelectableHitObject.cs new file mode 100644 index 000000000..5748f76a6 --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Catch/CatchSelectableHitObject.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.Objects; +using osuTK; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Framework.Input.Events; +using osuTK.Input; +using osu.Framework.Bindables; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Catch +{ + public partial class CatchSelectableHitObject : DrawableCatchHitObject + { + // This is HitCirclePiece instead of FruitOutline because FruitOutline doesn't register input for some reason + private HitCirclePiece outline = null!; + private SelectionState state; + + public readonly Bindable PlayfieldSelectedObject = new Bindable(); + + public CatchSelectableHitObject(CatchHitObject hitObject) + : base(hitObject) + { + X = hitObject.EffectiveX; + state = SelectionState.NotSelected; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(outline = new HitCirclePiece + { + Alpha = 0, + Scale = HitObject is Droplet ? new Vector2(HitObject.Scale) * 0.5f : new Vector2(HitObject.Scale) + }); + + PlayfieldSelectedObject.BindValueChanged(x => + { + if (x.NewValue != this) + { + Deselect(); + } + }); + } + + protected override bool OnClick(ClickEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + if (!IsHovered) + return false; + + if (state == SelectionState.Selected) + { + Deselect(); + PlayfieldSelectedObject.Value = null; + + return true; + } + + state = SelectionState.Selected; + outline.Show(); + PlayfieldSelectedObject.Value = this; + + return true; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => outline.ReceivePositionalInputAt(screenSpacePos); + + public override bool HandlePositionalInput => ShouldBeAlive || IsPresent; + + public void Deselect() + { + if (IsLoaded) + { + state = SelectionState.NotSelected; + outline.Hide(); + } + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/CatchObjectInspectorRuleset.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/CatchObjectInspectorRuleset.cs deleted file mode 100644 index ce397f736..000000000 --- a/PerformanceCalculatorGUI/Screens/ObjectInspection/CatchObjectInspectorRuleset.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; -using osu.Game.Rulesets.Catch.Edit; -using osu.Game.Rulesets.Mods; - -namespace PerformanceCalculatorGUI.Screens.ObjectInspection -{ - public partial class CatchObjectInspectorRuleset : DrawableCatchEditorRuleset - { - private readonly CatchDifficultyHitObject[] difficultyHitObjects; - - [Resolved] - private ObjectDifficultyValuesContainer objectDifficultyValuesContainer { get; set; } - - public CatchObjectInspectorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods, ExtendedCatchDifficultyCalculator difficultyCalculator, double clockRate) - : base(ruleset, beatmap, mods) - { - difficultyHitObjects = difficultyCalculator.GetDifficultyHitObjects(beatmap, clockRate) - .Cast().ToArray(); - } - - public override bool PropagatePositionalInputSubTree => false; - - public override bool PropagateNonPositionalInputSubTree => false; - - public override bool AllowBackwardsSeeks => true; - - protected override void Update() - { - base.Update(); - objectDifficultyValuesContainer.CurrentDifficultyHitObject.Value = difficultyHitObjects.LastOrDefault(x => x.StartTime < Clock.CurrentTime); - } - } -} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectDifficultyValuesContainer.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectDifficultyValuesContainer.cs index f14cae607..c55493bb7 100644 --- a/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectDifficultyValuesContainer.cs +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectDifficultyValuesContainer.cs @@ -94,6 +94,7 @@ private void updateValues(DifficultyHitObject hitObject) } hitObjectTypeText.Text = hitObject.BaseObject.GetType().Name; + flowContainer.Add(new ObjectInspectorDifficultyValue("Start Time", hitObject.StartTime)); switch (hitObject) { diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs index f52a1f655..7f402f16e 100644 --- a/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs @@ -24,6 +24,9 @@ using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osuTK.Input; +using PerformanceCalculatorGUI.Screens.ObjectInspection.Catch; +using PerformanceCalculatorGUI.Screens.ObjectInspection.Osu; +using PerformanceCalculatorGUI.Screens.ObjectInspection.Taiko; namespace PerformanceCalculatorGUI.Screens.ObjectInspection { diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/OsuObjectInspectorRuleset.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuObjectInspectorRuleset.cs similarity index 75% rename from PerformanceCalculatorGUI/Screens/ObjectInspection/OsuObjectInspectorRuleset.cs rename to PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuObjectInspectorRuleset.cs index ab775e3e6..9c74437fb 100644 --- a/PerformanceCalculatorGUI/Screens/ObjectInspection/OsuObjectInspectorRuleset.cs +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuObjectInspectorRuleset.cs @@ -12,18 +12,19 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; -namespace PerformanceCalculatorGUI.Screens.ObjectInspection +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Osu { public partial class OsuObjectInspectorRuleset : DrawableOsuEditorRuleset { private readonly OsuDifficultyHitObject[] difficultyHitObjects; [Resolved] - private ObjectDifficultyValuesContainer objectDifficultyValuesContainer { get; set; } + private ObjectDifficultyValuesContainer difficultyValuesContainer { get; set; } public OsuObjectInspectorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods, ExtendedOsuDifficultyCalculator difficultyCalculator, double clockRate) : base(ruleset, beatmap, mods) @@ -31,14 +32,15 @@ public OsuObjectInspectorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyLis difficultyHitObjects = difficultyCalculator.GetDifficultyHitObjects(beatmap, clockRate).Cast().ToArray(); } - protected override void Update() + protected override void LoadComplete() { - base.Update(); - objectDifficultyValuesContainer.CurrentDifficultyHitObject.Value = difficultyHitObjects.LastOrDefault(x => x.StartTime <= Clock.CurrentTime); + base.LoadComplete(); + KeyBindingInputManager.AllowGameplayInputs = false; + ((OsuObjectInspectorPlayfield)Playfield).Pool.SelectedObject.BindValueChanged(value => + difficultyValuesContainer.CurrentDifficultyHitObject.Value = difficultyHitObjects.FirstOrDefault(x => x.BaseObject == value.NewValue)); } - public override bool PropagatePositionalInputSubTree => false; - + public override bool PropagatePositionalInputSubTree => true; public override bool PropagateNonPositionalInputSubTree => false; public override bool AllowBackwardsSeeks => true; @@ -48,15 +50,42 @@ protected override void Update() private partial class OsuObjectInspectorPlayfield : OsuPlayfield { private readonly IReadOnlyList difficultyHitObjects; - protected override GameplayCursorContainer CreateCursor() => null; + + public OsuSelectableObjectPool Pool { get; private set; } public OsuObjectInspectorPlayfield(IReadOnlyList difficultyHitObjects) { this.difficultyHitObjects = difficultyHitObjects; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(Pool = new OsuSelectableObjectPool { RelativeSizeAxes = Axes.Both }); HitPolicy = new AnyOrderHitPolicy(); DisplayJudgements.Value = false; } + protected override GameplayCursorContainer CreateCursor() => null; + + protected override void OnHitObjectAdded(HitObject hitObject) + { + base.OnHitObjectAdded(hitObject); + + if (hitObject is Spinner) return; + + Pool.AddSelectableObject((OsuHitObject)hitObject); + } + + protected override void OnHitObjectRemoved(HitObject hitObject) + { + base.OnHitObjectRemoved(hitObject); + + if (hitObject is Spinner) return; + + Pool.RemoveSelectableObject((OsuHitObject)hitObject); + } + protected override void OnNewDrawableHitObject(DrawableHitObject d) { base.OnNewDrawableHitObject(d); diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableHitObject.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableHitObject.cs new file mode 100644 index 000000000..071716d4f --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableHitObject.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Objects.Pooling; +using osu.Game.Rulesets.Osu.Objects; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Osu +{ + public abstract partial class OsuSelectableHitObject : PoolableDrawableWithLifetime + { + public abstract OsuHitObject? HitObject { get; protected set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + UpdateState(); + } + + public abstract void UpdateFromHitObject(); + + protected override void OnApply(OsuSelectableObjectLifetimeEntry entry) + { + HitObject = entry.HitObject; + UpdateFromHitObject(); + } + + #region Selection Logic + + protected override bool ShouldBeAlive => base.ShouldBeAlive || IsSelected; + public override bool RemoveCompletedTransforms => true; // To prevent selecting when rewinding back + public override bool HandlePositionalInput => ShouldBeAlive || IsPresent; + + private SelectionState state; + + public SelectionState State + { + get => state; + set + { + if (state == value) + return; + + state = value; + + if (IsLoaded) + UpdateState(); + } + } + + public void UpdateState() + { + switch (state) + { + case SelectionState.Selected: + OnSelected(); + break; + + case SelectionState.NotSelected: + OnDeselected(); + break; + } + } + + protected void OnDeselected() + { + foreach (var d in InternalChildren) + d.Hide(); + } + + protected void OnSelected() + { + foreach (var d in InternalChildren) + d.Show(); + } + + //protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected; + public void Select() => State = SelectionState.Selected; + public void Deselect() => State = SelectionState.NotSelected; + public bool IsSelected => State == SelectionState.Selected; + + #endregion + } + + public abstract partial class OsuSelectableHitObject : OsuSelectableHitObject + where THitObject : OsuHitObject + { + private THitObject? hitObject; + + public override OsuHitObject? HitObject + { + get => hitObject; + protected set => hitObject = (THitObject?)value; + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableObjectLifetimeEntry.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableObjectLifetimeEntry.cs new file mode 100644 index 000000000..852ff8e70 --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableObjectLifetimeEntry.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Osu +{ + public class OsuSelectableObjectLifetimeEntry : LifetimeEntry + { + private const int hit_object_fade_out_extension = 200; + public OsuHitObject HitObject { get; private set; } + + public OsuSelectableObjectLifetimeEntry(OsuHitObject hitObject) + { + HitObject = hitObject; + RefreshLifetimes(); + } + + protected void RefreshLifetimes() + { + SetLifetimeStart(HitObject.StartTime - HitObject.TimePreempt); + SetLifetimeEnd(HitObject.GetEndTime() + hit_object_fade_out_extension); + } + + // The lifetime, as set by the hitobject. + private double realLifetimeStart = double.MinValue; + private double realLifetimeEnd = double.MaxValue; + + // This method is called even if `start == LifetimeStart` when `KeepAlive` is true (necessary to update `realLifetimeStart`). + protected override void SetLifetimeStart(double start) + { + realLifetimeStart = start; + if (!keepAlive) + base.SetLifetimeStart(start); + } + + protected override void SetLifetimeEnd(double end) + { + realLifetimeEnd = end; + if (!keepAlive) + base.SetLifetimeEnd(end); + } + + private bool keepAlive; + + public bool KeepAlive + { + set + { + if (keepAlive == value) + return; + + keepAlive = value; + if (keepAlive) + SetLifetime(double.MinValue, double.MaxValue); + else + SetLifetime(realLifetimeStart, realLifetimeEnd); + } + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableObjectPool.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableObjectPool.cs new file mode 100644 index 000000000..395200a4a --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/OsuSelectableObjectPool.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Objects.Pooling; +using osu.Framework.Input.Events; +using osuTK.Input; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Osu.Objects; +using System.Diagnostics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Allocation; +using osu.Framework.Graphics; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Osu +{ + public partial class OsuSelectableObjectPool : PooledDrawableWithLifetimeContainer + { + public readonly Bindable SelectedObject = new Bindable(); + public override bool HandlePositionalInput => true; + + private DrawablePool circlesPool; + private DrawablePool slidersPool; + + public OsuSelectableObjectPool() + { + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + circlesPool = new DrawablePool(1, 200), + slidersPool = new DrawablePool(1, 200) + }; + } + + protected override bool OnClick(ClickEvent e) + { + if (e.Button == MouseButton.Right) + return false; + + // Variable for handling selection of desired object in the stack (otherwise it will iterate between 2) + var wasSelectedJustDeselected = false; + + KeyValuePair? newSelectedEntry = null; + + foreach (var entry in AliveEntries.OrderBy(pair => pair.Value.HitObject?.StartTime)) + { + var lifetimeEntry = entry.Key; + var blueprint = entry.Value; + + if (blueprint.IsSelected) + { + lifetimeEntry.KeepAlive = false; + blueprint.Deselect(); + wasSelectedJustDeselected = true; + continue; + } + + if (newSelectedEntry != null && !wasSelectedJustDeselected) + continue; + + if (!blueprint.IsHovered) + continue; + + newSelectedEntry?.Value.Deselect(); + blueprint.Select(); + + newSelectedEntry = entry; + wasSelectedJustDeselected = false; + } + + if (newSelectedEntry.IsNotNull()) newSelectedEntry.Value.Key.KeepAlive = true; + SelectedObject.Value = newSelectedEntry?.Value.HitObject; + return true; + } + + protected override OsuSelectableHitObject GetDrawable(OsuSelectableObjectLifetimeEntry entry) + { + OsuSelectableHitObject? result = entry.HitObject switch + { + HitCircle => circlesPool.Get(), + Slider => slidersPool.Get(), + _ => null + }; + + // Entry shouldn't be create for not supported hitobject types + Debug.Assert(result != null); + + result.Apply(entry); + return result; + } + + public OsuSelectableObjectLifetimeEntry CreateEntry(OsuHitObject hitObject) => new OsuSelectableObjectLifetimeEntry(hitObject); + + public void AddSelectableObject(OsuHitObject hitObject) + { + var newEntry = CreateEntry(hitObject); + Add(newEntry); + } + + public void RemoveSelectableObject(OsuHitObject hitObject) + { + var entry = Entries.First(e => e.HitObject == hitObject); + Remove(entry); + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/SelectableHitCircle.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/SelectableHitCircle.cs new file mode 100644 index 000000000..5af06c7e4 --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/SelectableHitCircle.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; +using osu.Framework.Allocation; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Osu +{ + public partial class SelectableHitCircle : OsuSelectableHitObject + { + private HitCirclePiece circlePiece = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = circlePiece = new HitCirclePiece(); + UpdateFromHitObject(); + } + + public override void UpdateFromHitObject() + { + if (HitObject != null) + circlePiece.UpdateFrom((HitCircle)HitObject); + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circlePiece.ReceivePositionalInputAt(screenSpacePos); + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/SelectableSlider.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/SelectableSlider.cs new file mode 100644 index 000000000..d8e24428e --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Osu/SelectableSlider.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Game.Rulesets.Osu.Objects; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osuTK; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Game.Rulesets.Osu.Edit.Blueprints; +using osu.Game.Graphics; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osuTK.Graphics; +using System.Collections.Generic; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Osu +{ + public partial class SelectableSlider : OsuSelectableHitObject + { + private CustomSliderBodyPiece bodyPiece = null!; + private HitCirclePiece headOverlay = null!; + private HitCirclePiece tailOverlay = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + bodyPiece = new CustomSliderBodyPiece(), + headOverlay = new HitCirclePiece(), + tailOverlay = new HitCirclePiece(), + }; + UpdateFromHitObject(); + } + + public override void UpdateFromHitObject() + { + if (HitObject == null) return; + + var slider = (Slider)HitObject; + + bodyPiece.UpdateFrom(slider); + headOverlay.UpdateFrom(slider.HeadCircle); + tailOverlay.UpdateFrom(slider.TailCircle); + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => bodyPiece.ReceivePositionalInputAt(screenSpacePos); + + private partial class CustomSliderBodyPiece : BlueprintPiece + { + private readonly ManualSliderBody body; + + public CustomSliderBodyPiece() + { + AutoSizeAxes = Axes.Both; + + AlwaysPresent = true; + + InternalChild = body = new ManualSliderBody + { + AccentColour = Color4.Transparent + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + body.BorderColour = colours.Yellow; + } + + public override void UpdateFrom(Slider hitObject) + { + base.UpdateFrom(hitObject); + + body.PathRadius = hitObject.Scale * OsuHitObject.OBJECT_RADIUS; + + var vertices = new List(); + hitObject.Path.GetPathToProgress(vertices, 0, 1); + + body.SetVertices(vertices); + + OriginPosition = body.PathOffset; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos); + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Taiko/TaikoObjectInspectorRuleset.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Taiko/TaikoObjectInspectorRuleset.cs new file mode 100644 index 000000000..fb4077ad2 --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Taiko/TaikoObjectInspectorRuleset.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Edit; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osuTK.Input; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Taiko +{ + public partial class TaikoObjectInspectorRuleset : DrawableTaikoEditorRuleset + { + private readonly TaikoDifficultyHitObject[] difficultyHitObjects; + private TaikoObjectInspectorPlayfield inspectorPlayfield; + + [Resolved] + private ObjectDifficultyValuesContainer difficultyValuesContainer { get; set; } + + public TaikoObjectInspectorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods, ExtendedTaikoDifficultyCalculator difficultyCalculator, double clockRate) + : base(ruleset, beatmap, mods) + { + difficultyHitObjects = difficultyCalculator.GetDifficultyHitObjects(beatmap, clockRate) + .Cast().ToArray(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inspectorPlayfield.SelectedObject.BindValueChanged(value => + { + difficultyValuesContainer.CurrentDifficultyHitObject.Value = difficultyHitObjects.FirstOrDefault(x => x.BaseObject.StartTime == value.NewValue?.HitObject.StartTime); + }); + } + + public override bool PropagatePositionalInputSubTree => true; + + public override bool PropagateNonPositionalInputSubTree => false; + + public override bool AllowBackwardsSeeks => true; + + protected override Playfield CreatePlayfield() => inspectorPlayfield = new TaikoObjectInspectorPlayfield(); + + protected override PassThroughInputManager CreateInputManager() => new TaikoObjectInspectorInputManager(Ruleset.RulesetInfo); + + private partial class TaikoObjectInspectorInputManager : TaikoInputManager + { + public TaikoObjectInspectorInputManager(RulesetInfo ruleset) + : base(ruleset) + { + } + + protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new EmptyKeyBindingContainer(ruleset, variant, unique); + + private partial class EmptyKeyBindingContainer : RulesetKeyBindingContainer + { + public EmptyKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override void ReloadMappings(IQueryable realmKeyBindings) + { + base.ReloadMappings(realmKeyBindings); + KeyBindings = Enumerable.Empty(); + } + } + } + + private partial class TaikoObjectInspectorPlayfield : TaikoPlayfield + { + public readonly Bindable SelectedObject = new Bindable(); + + public TaikoObjectInspectorPlayfield() + { + DisplayJudgements.Value = false; + } + + protected override void OnHitObjectAdded(HitObject hitObject) + { + base.OnHitObjectAdded(hitObject); + + TaikoSelectableHitObject newSelectable = hitObject switch + HitObjectContainer.Add(new TaikoSelectableHitObject((TaikoHitObject)hitObject) + { + PlayfieldSelectedObject = { BindTarget = SelectedObject } + }); + } + + protected override GameplayCursorContainer CreateCursor() => null; + + protected override bool OnClick(ClickEvent e) + { + if (e.Button == MouseButton.Right) + return false; + + SelectedObject.Value?.Deselect(); + SelectedObject.Value = null; + return false; + } + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/Taiko/TaikoSelectableHitObject.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/Taiko/TaikoSelectableHitObject.cs new file mode 100644 index 000000000..3bbdb1e3d --- /dev/null +++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/Taiko/TaikoSelectableHitObject.cs @@ -0,0 +1,103 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Edit.Blueprints; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; +using osuTK; +using osuTK.Input; + +namespace PerformanceCalculatorGUI.Screens.ObjectInspection.Taiko +{ + public partial class TaikoSelectableHitObject : DrawableTaikoHitObject + { + private HitPiece hitPiece; + private SelectionState state; + private readonly bool isStrong; + + public readonly Bindable PlayfieldSelectedObject = new Bindable(); + + public TaikoSelectableHitObject(TaikoHitObject hitObject) + : base(hitObject) + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + state = SelectionState.NotSelected; + isStrong = hitObject is TaikoStrongableHitObject; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(hitPiece = new HitPiece + { + Alpha = 0, + Size = getObjectSize() + }); + + PlayfieldSelectedObject.BindValueChanged(x => + { + if (x.NewValue != this) + { + Deselect(); + } + }); + } + + private Vector2 getObjectSize() + { + if (isStrong) + return new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE * TaikoPlayfield.BASE_HEIGHT); + + return new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.BASE_HEIGHT); + } + + protected override bool OnClick(ClickEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + if (!IsHovered) + return false; + + if (state == SelectionState.Selected) + { + Deselect(); + PlayfieldSelectedObject.Value = null; + + return true; + } + + state = SelectionState.Selected; + hitPiece.Show(); + PlayfieldSelectedObject.Value = this; + + return true; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => hitPiece.ReceivePositionalInputAt(screenSpacePos); + + public override bool OnPressed(KeyBindingPressEvent e) => true; + + public override bool HandlePositionalInput => ShouldBeAlive || IsPresent; + + public void Deselect() + { + if (IsLoaded) + { + state = SelectionState.NotSelected; + hitPiece.Hide(); + } + } + } +} diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/TaikoObjectInspectorRuleset.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/TaikoObjectInspectorRuleset.cs deleted file mode 100644 index 34f9646a9..000000000 --- a/PerformanceCalculatorGUI/Screens/ObjectInspection/TaikoObjectInspectorRuleset.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Edit; -using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Rulesets.UI; - -namespace PerformanceCalculatorGUI.Screens.ObjectInspection -{ - public partial class TaikoObjectInspectorRuleset : DrawableTaikoEditorRuleset - { - private readonly TaikoDifficultyHitObject[] difficultyHitObjects; - - [Resolved] - private ObjectDifficultyValuesContainer objectDifficultyValuesContainer { get; set; } - - public TaikoObjectInspectorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods, ExtendedTaikoDifficultyCalculator difficultyCalculator, double clockRate) - : base(ruleset, beatmap, mods) - { - difficultyHitObjects = difficultyCalculator.GetDifficultyHitObjects(beatmap, clockRate) - .Cast().ToArray(); - } - - public override bool PropagatePositionalInputSubTree => false; - - public override bool PropagateNonPositionalInputSubTree => false; - - public override bool AllowBackwardsSeeks => true; - - protected override Playfield CreatePlayfield() => new TaikoObjectInspectorPlayfield(); - - protected override void Update() - { - base.Update(); - objectDifficultyValuesContainer.CurrentDifficultyHitObject.Value = difficultyHitObjects.LastOrDefault(x => x.StartTime < Clock.CurrentTime); - } - - private partial class TaikoObjectInspectorPlayfield : TaikoPlayfield - { - protected override GameplayCursorContainer CreateCursor() => null; - - public TaikoObjectInspectorPlayfield() - { - DisplayJudgements.Value = false; - } - } - } -}