using System;
using System.Collections.Generic;
using System.Linq;
using OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI
{
///
/// A type of specialized towards scrolling .
///
public class ScrollingPlayfield : Playfield
where TObject : HitObject
where TJudgement : Judgement
{
///
/// The default span of time visible by the length of the scrolling axes.
/// This is clamped between and .
///
private const double time_span_default = 1500;
///
/// The minimum span of time that may be visible by the length of the scrolling axes.
///
private const double time_span_min = 50;
///
/// The maximum span of time that may be visible by the length of the scrolling axes.
///
private const double time_span_max = 10000;
///
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
///
private const double time_span_step = 50;
///
/// Gets or sets the range of time that is visible by the length of the scrolling axes.
/// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000.
///
private readonly BindableDouble visibleTimeRange = new BindableDouble(time_span_default)
{
Default = time_span_default,
MinValue = time_span_min,
MaxValue = time_span_max
};
///
/// The span of time visible by the length of the scrolling axes.
///
///
public BindableDouble VisibleTimeRange
{
get { return visibleTimeRange; }
set { visibleTimeRange.BindTo(value); }
}
///
/// The container that contains the s and s.
///
internal new readonly ScrollingHitObjectContainer HitObjects;
///
/// Creates a new .
///
/// The axes on which s in this container should scroll.
/// Whether we want our internal coordinate system to be scaled to a specified width
protected ScrollingPlayfield(Axes scrollingAxes, float? customWidth = null)
: base(customWidth)
{
base.HitObjects = HitObjects = new ScrollingHitObjectContainer(scrollingAxes)
{
RelativeSizeAxes = Axes.Both,
VisibleTimeRange = VisibleTimeRange
};
}
private List> nestedPlayfields;
///
/// All the s nested inside this playfield.
///
public IEnumerable> NestedPlayfields => nestedPlayfields;
///
/// Adds a to this playfield. The nested
/// will be given all of the same speed adjustments as this playfield.
///
/// The to add.
protected void AddNested(ScrollingPlayfield otherPlayfield)
{
if (nestedPlayfields == null)
nestedPlayfields = new List>();
nestedPlayfields.Add(otherPlayfield);
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (state.Keyboard.ControlPressed)
{
switch (args.Key)
{
case Key.Minus:
transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
break;
case Key.Plus:
transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
break;
}
}
return false;
}
private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None)
{
this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
}
private class TransformVisibleTimeRange : Transform>
{
private double valueAt(double time)
{
if (time < StartTime) return StartValue;
if (time >= EndTime) return EndValue;
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
}
public override string TargetMember => "VisibleTimeRange.Value";
protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time);
protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value;
}
///
/// A container that provides the foundation for sorting s into s.
///
internal class ScrollingHitObjectContainer : HitObjectContainer>
{
private readonly BindableDouble visibleTimeRange = new BindableDouble { Default = 1000 };
///
/// Gets or sets the range of time that is visible by the length of the scrolling axes.
/// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000.
///
public Bindable VisibleTimeRange
{
get { return visibleTimeRange; }
set { visibleTimeRange.BindTo(value); }
}
protected override Container> Content => content;
private Container> content;
///
/// Hit objects that are to be re-processed on the next update.
///
private readonly Queue> queuedHitObjects = new Queue>();
private readonly Axes scrollingAxes;
///
/// Creates a new .
///
/// The axes upon which hit objects should appear to scroll inside this container.
public ScrollingHitObjectContainer(Axes scrollingAxes)
{
this.scrollingAxes = scrollingAxes;
// The following is never used - it only exists for the purpose of being able to use AddInternal below.
content = new Container>();
}
///
/// Adds a to this container.
///
/// The .
public void AddSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment)
{
speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange);
speedAdjustment.ScrollingAxes = scrollingAxes;
AddInternal(speedAdjustment);
}
///
/// Adds a hit object to this . The hit objects will be queued to be processed
/// new s are added to this .
///
/// The hit object to add.
public override void Add(DrawableHitObject hitObject)
{
if (!(hitObject is IScrollingHitObject))
throw new InvalidOperationException($"Hit objects added to a {nameof(ScrollingHitObjectContainer)} must implement {nameof(IScrollingHitObject)}.");
queuedHitObjects.Enqueue(hitObject);
}
protected override void Update()
{
base.Update();
// Todo: At the moment this is going to re-process every single Update, however this will only be a null-op
// when there are no SpeedAdjustmentContainers available. This should probably error or something, but it's okay for now.
// An external count is kept because hit objects that can't be added are re-queued
int count = queuedHitObjects.Count;
while (count-- > 0)
{
var hitObject = queuedHitObjects.Dequeue();
var target = adjustmentContainerFor(hitObject);
if (target == null)
{
// We can't add this hit object to a speed adjustment container yet, so re-queue it
// for re-processing when the layout next invalidated
queuedHitObjects.Enqueue(hitObject);
continue;
}
target.Add(hitObject);
}
}
///
/// Finds the which provides the speed adjustment active at the start time
/// of a hit object. If there is no active at the start time of the hit object,
/// then the first (time-wise) speed adjustment is returned.
///
/// The hit object to find the active for.
/// The active at 's start time. Null if there are no speed adjustments.
private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => InternalChildren.OfType().FirstOrDefault(c => c.CanContain(hitObject)) ?? InternalChildren.OfType().LastOrDefault();
///
/// Finds the which provides the speed adjustment active at a time.
/// If there is no active at the time, then the first (time-wise) speed adjustment is returned.
///
/// The time to find the active at.
/// The active at . Null if there are no speed adjustments.
private SpeedAdjustmentContainer adjustmentContainerAt(double time) => InternalChildren.OfType().FirstOrDefault(c => c.CanContain(time)) ?? InternalChildren.OfType().LastOrDefault();
}
}
}