Merge branch 'master' into improve-dho-time-offsets

This commit is contained in:
smoogipoo
2020-11-30 18:01:48 +09:00
41 changed files with 527 additions and 240 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1120.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1127.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK; using osuTK;
@ -17,34 +18,49 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
base.LoadComplete(); base.LoadComplete();
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep("show pear", () => SetContents(() => createDrawableFruit(0)));
AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep))); AddStep("show grape", () => SetContents(() => createDrawableFruit(1)));
AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2)));
AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3)));
AddStep("show banana", () => SetContents(createDrawableBanana));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true)));
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true))); AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true)));
AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true)));
AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
} }
private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash); SetProperties(new DrawableFruit(new Fruit
{
IndexInBeatmap = indexInBeatmap,
HyperDashBindable = { Value = hyperdash }
}));
private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); private Drawable createDrawableBanana() =>
SetProperties(new DrawableBanana(new Banana()));
private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet())); private Drawable createDrawableDroplet(bool hyperdash = false) =>
SetProperties(new DrawableDroplet(new Droplet
{
HyperDashBindable = { Value = hyperdash }
}));
private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false) private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet()));
protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d)
{ {
var hitObject = d.HitObject; var hitObject = d.HitObject;
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 });
hitObject.StartTime = 1000000000000; hitObject.StartTime = 1000000000000;
hitObject.Scale = 1.5f; hitObject.Scale = 1.5f;
if (hyperdash)
((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana();
d.Anchor = Anchor.Centre; d.Anchor = Anchor.Centre;
d.RelativePositionAxes = Axes.None; d.RelativePositionAxes = Axes.None;
d.Position = Vector2.Zero; d.Position = Vector2.Zero;
@ -55,15 +71,5 @@ namespace osu.Game.Rulesets.Catch.Tests
}; };
return d; return d;
} }
public class TestCatchFruit : Fruit
{
public TestCatchFruit(FruitVisualRepresentation rep)
{
VisualRepresentation = rep;
}
public override FruitVisualRepresentation VisualRepresentation { get; }
}
} }
} }

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneFruitVisualChange : TestSceneFruitObjects
{
private readonly Bindable<int> indexInBeatmap = new Bindable<int>();
private readonly Bindable<bool> hyperDash = new Bindable<bool>();
protected override void LoadComplete()
{
AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit
{
IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
HyperDashBindable = { BindTarget = hyperDash },
}))));
AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet
{
HyperDashBindable = { BindTarget = hyperDash },
}))));
Scheduler.AddDelayed(() => indexInBeatmap.Value++, 250, true);
Scheduler.AddDelayed(() => hyperDash.Value = !hyperDash.Value, 1000, true);
}
}
}

View File

@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public int BananaIndex; public int BananaIndex;
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement(); public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }; private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };

View File

@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public class BananaShower : CatchHitObject, IHasDuration public class BananaShower : CatchHitObject, IHasDuration
{ {
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override bool LastInCombo => true; public override bool LastInCombo => true;
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();

View File

@ -16,27 +16,47 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;
private float x; // This value is after XOffset applied.
public readonly Bindable<float> XBindable = new Bindable<float>();
// This value is before XOffset applied.
private float originalX;
/// <summary> /// <summary>
/// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>. /// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary> /// </summary>
public float X public float X
{ {
get => x + XOffset; // TODO: I don't like this asymmetry.
set => x = value; get => XBindable.Value;
// originalX is set by `XBindable.BindValueChanged`
set => XBindable.Value = value + xOffset;
} }
private float xOffset;
/// <summary> /// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>. /// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// </summary> /// </summary>
internal float XOffset { get; set; } internal float XOffset
{
get => xOffset;
set
{
xOffset = value;
XBindable.Value = originalX + xOffset;
}
}
public double TimePreempt = 1000; public double TimePreempt = 1000;
public int IndexInBeatmap { get; set; } public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>();
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); public int IndexInBeatmap
{
get => IndexInBeatmapBindable.Value;
set => IndexInBeatmapBindable.Value = value;
}
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
@ -69,7 +89,13 @@ namespace osu.Game.Rulesets.Catch.Objects
set => LastInComboBindable.Value = value; set => LastInComboBindable.Value = value;
} }
public float Scale { get; set; } = 1; public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public float Scale
{
get => ScaleBindable.Value;
set => ScaleBindable.Value = value;
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
@ -81,14 +107,10 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
public enum FruitVisualRepresentation protected CatchHitObject()
{ {
Pear, XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset);
Grape, }
Pineapple,
Raspberry,
Banana // banananananannaanana
} }
} }

View File

@ -8,6 +8,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableBanana : DrawableFruit public class DrawableBanana : DrawableFruit
{ {
protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana;
public DrawableBanana(Banana h) public DrawableBanana(Banana h)
: base(h) : base(h)
{ {

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -10,19 +12,34 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject> public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{ {
public readonly Bindable<float> XBindable = new Bindable<float>();
protected override double InitialLifetimeOffset => HitObject.TimePreempt; protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected DrawableCatchHitObject(CatchHitObject hitObject) protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
X = hitObject.X;
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
} }
protected override void OnApply()
{
base.OnApply();
XBindable.BindTo(HitObject.XBindable);
}
protected override void OnFree()
{
base.OnFree();
XBindable.UnbindFrom(HitObject.XBindable);
}
public Func<CatchHitObject, bool> CheckPosition; public Func<CatchHitObject, bool> CheckPosition;
public bool IsOnPlate; public bool IsOnPlate;

View File

@ -21,7 +21,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece()); HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Droplet),
_ => new DropletPiece
{
HyperDash = { BindTarget = HyperDash }
});
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -11,6 +12,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : DrawablePalpableCatchHitObject public class DrawableFruit : DrawablePalpableCatchHitObject
{ {
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
public DrawableFruit(CatchHitObject h) public DrawableFruit(CatchHitObject h)
: base(h) : base(h)
{ {
@ -19,10 +24,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
IndexInBeatmap.BindValueChanged(change =>
{
VisualRepresentation.Value = GetVisualRepresentation(change.NewValue);
}, true);
VisualRepresentation.BindValueChanged(_ => updatePiece());
HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(getComponent(VisualRepresentation.Value)),
_ => new FruitPiece
{
VisualRepresentation = { BindTarget = VisualRepresentation },
HyperDash = { BindTarget = HyperDash },
});
} }
private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
@ -49,4 +70,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
} }
} }
} }
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
Banana // banananananannaanana
}
} }

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osuTK; using osuTK;
@ -12,6 +14,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
/// <summary>
/// The multiplicative factor applied to <see cref="ScaleContainer"/> scale relative to <see cref="HitObject"/> scale.
/// </summary>
protected virtual float ScaleFactor => 1;
/// <summary> /// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
/// </summary> /// </summary>
@ -19,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected readonly Container ScaleContainer; protected readonly Container ScaleContainer;
protected DrawablePalpableCatchHitObject(CatchHitObject h) protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)
: base(h) : base(h)
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -36,7 +49,35 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Scale = new Vector2(HitObject.Scale); XBindable.BindValueChanged(x =>
{
if (!IsOnPlate) X = x.NewValue;
}, true);
ScaleBindable.BindValueChanged(scale =>
{
ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
}, true);
IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());
}
protected override void OnApply()
{
base.OnApply();
HyperDash.BindTo(HitObject.HyperDashBindable);
ScaleBindable.BindTo(HitObject.ScaleBindable);
IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable);
}
protected override void OnFree()
{
HyperDash.UnbindFrom(HitObject.HyperDashBindable);
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable);
base.OnFree();
} }
} }
} }

View File

@ -1,21 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableTinyDroplet : DrawableDroplet public class DrawableTinyDroplet : DrawableDroplet
{ {
protected override float ScaleFactor => base.ScaleFactor / 2;
public DrawableTinyDroplet(TinyDroplet h) public DrawableTinyDroplet(TinyDroplet h)
: base(h) : base(h)
{ {
} }
[BackgroundDependencyLoader]
private void load()
{
ScaleContainer.Scale /= 2;
}
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class DropletPiece : CompositeDrawable public class DropletPiece : CompositeDrawable
{ {
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public DropletPiece() public DropletPiece()
{ {
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
@ -19,15 +22,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject) private void load(DrawableHitObject drawableObject)
{ {
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
InternalChild = new Pulp InternalChild = new Pulp
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour } AccentColour = { BindTarget = drawableObject.AccentColour }
}; };
if (drawableCatchObject.HitObject.HyperDash) if (HyperDash.Value)
{ {
AddInternal(new HyperDropletBorderPiece()); AddInternal(new HyperDropletBorderPiece());
} }

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -16,36 +18,39 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
/// </summary> /// </summary>
public const float RADIUS_ADJUST = 1.1f; public const float RADIUS_ADJUST = 1.1f;
private BorderPiece border; public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
private PalpableCatchHitObject hitObject; public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[CanBeNull]
private DrawableCatchHitObject drawableHitObject;
[CanBeNull]
private BorderPiece borderPiece;
public FruitPiece() public FruitPiece()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(permitNulls: true)]
private void load(DrawableHitObject drawableObject) private void load([CanBeNull] DrawableHitObject drawable)
{ {
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; drawableHitObject = (DrawableCatchHitObject)drawable;
hitObject = drawableCatchObject.HitObject;
AddRangeInternal(new[] AddInternal(getFruitFor(VisualRepresentation.Value));
{
getFruitFor(hitObject.VisualRepresentation),
border = new BorderPiece(),
});
if (hitObject.HyperDash) // if it is not part of a DHO, the border is always invisible.
{ if (drawableHitObject != null)
AddInternal(borderPiece = new BorderPiece());
if (HyperDash.Value)
AddInternal(new HyperBorderPiece()); AddInternal(new HyperBorderPiece());
}
} }
protected override void Update() protected override void Update()
{ {
base.Update(); if (borderPiece != null && drawableHitObject?.HitObject != null)
border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1); borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1);
} }
private Drawable getFruitFor(FruitVisualRepresentation representation) private Drawable getFruitFor(FruitVisualRepresentation representation)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics; using osuTK.Graphics;
@ -20,15 +21,27 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public float DistanceToHyperDash { get; set; } public float DistanceToHyperDash { get; set; }
public readonly Bindable<bool> HyperDashBindable = new Bindable<bool>();
/// <summary> /// <summary>
/// Whether this fruit can initiate a hyperdash. /// Whether this fruit can initiate a hyperdash.
/// </summary> /// </summary>
public bool HyperDash => HyperDashTarget != null; public bool HyperDash => HyperDashBindable.Value;
private CatchHitObject hyperDashTarget;
/// <summary> /// <summary>
/// The target fruit if we are to initiate a hyperdash. /// The target fruit if we are to initiate a hyperdash.
/// </summary> /// </summary>
public CatchHitObject HyperDashTarget; public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;
set
{
hyperDashTarget = value;
HyperDashBindable.Value = value != null;
}
}
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
} }

View File

@ -55,7 +55,13 @@ namespace osu.Game.Rulesets.Catch.UI
HitObjectContainer, HitObjectContainer,
CatcherArea, CatcherArea,
}; };
}
protected override void LoadComplete()
{
base.LoadComplete();
// these subscriptions need to be done post constructor to ensure externally bound components have a chance to populate required fields (ScoreProcessor / ComboAtJudgement in this case).
NewResult += onNewResult; NewResult += onNewResult;
RevertResult += onRevertResult; RevertResult += onRevertResult;
} }

View File

@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
} }
private void trackingChanged(ValueChangedEvent<bool> tracking) => private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.6f : 0.05f, 200, Easing.OutQuint); box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private SkinnableDrawable mascot; private SkinnableDrawable mascot;
private ProxyContainer topLevelHitContainer; private ProxyContainer topLevelHitContainer;
private ProxyContainer barlineContainer; private ScrollingHitObjectContainer barlineContainer;
private Container rightArea; private Container rightArea;
private Container leftArea; private Container leftArea;
@ -83,10 +84,7 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
barlineContainer = new ProxyContainer barlineContainer = new ScrollingHitObjectContainer(),
{
RelativeSizeAxes = Axes.Both,
},
new Container new Container
{ {
Name = "Hit objects", Name = "Hit objects",
@ -159,18 +157,37 @@ namespace osu.Game.Rulesets.Taiko.UI
public override void Add(DrawableHitObject h) public override void Add(DrawableHitObject h)
{ {
h.OnNewResult += OnNewResult;
base.Add(h);
switch (h) switch (h)
{ {
case DrawableBarLine barline: case DrawableBarLine barline:
barlineContainer.Add(barline.CreateProxy()); barlineContainer.Add(barline);
break; break;
case DrawableTaikoHitObject taikoObject: case DrawableTaikoHitObject taikoObject:
h.OnNewResult += OnNewResult;
topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); topLevelHitContainer.Add(taikoObject.CreateProxiedContent());
base.Add(h);
break; break;
default:
throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type");
}
}
public override bool Remove(DrawableHitObject h)
{
switch (h)
{
case DrawableBarLine barline:
return barlineContainer.Remove(barline);
case DrawableTaikoHitObject _:
h.OnNewResult -= OnNewResult;
// todo: consider tidying of proxied content if required.
return base.Remove(h);
default:
throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type");
} }
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -44,6 +43,36 @@ namespace osu.Game.Tests.Editing
Assert.That(stateChangedFired, Is.EqualTo(2)); Assert.That(stateChangedFired, Is.EqualTo(2));
} }
[Test]
public void TestApplyThenUndoThenApplySameChange()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
string originalHash = handler.CurrentStateHash;
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1));
string hash = handler.CurrentStateHash;
// undo a change without saving
handler.RestoreState(-1);
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(2));
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
}
[Test] [Test]
public void TestSaveSameStateDoesNotSave() public void TestSaveSameStateDoesNotSave()
{ {
@ -139,7 +168,7 @@ namespace osu.Game.Tests.Editing
private void addArbitraryChange(EditorBeatmap beatmap) private void addArbitraryChange(EditorBeatmap beatmap)
{ {
beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); beatmap.Add(new HitCircle { StartTime = 2760 });
} }
} }
} }

View File

@ -3,7 +3,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -14,44 +13,41 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture] [TestFixture]
public class TestSceneStarCounter : OsuTestScene public class TestSceneStarCounter : OsuTestScene
{ {
private readonly StarCounter starCounter;
private readonly OsuSpriteText starsLabel;
public TestSceneStarCounter() public TestSceneStarCounter()
{ {
StarCounter stars = new StarCounter starCounter = new StarCounter
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Current = 5,
}; };
Add(stars); Add(starCounter);
SpriteText starsLabel = new OsuSpriteText starsLabel = new OsuSpriteText
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Scale = new Vector2(2), Scale = new Vector2(2),
Y = 50, Y = 50,
Text = stars.Current.ToString("0.00"),
}; };
Add(starsLabel); Add(starsLabel);
AddRepeatStep(@"random value", delegate setStars(5);
{
stars.Current = RNG.NextSingle() * (stars.StarCount + 1);
starsLabel.Text = stars.Current.ToString("0.00");
}, 10);
AddStep(@"Stop animation", delegate AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10);
{ AddSliderStep("exact value", 0f, 10f, 5f, setStars);
stars.StopAnimation(); AddStep("stop animation", () => starCounter.StopAnimation());
}); AddStep("reset", () => setStars(0));
}
AddStep(@"Reset", delegate private void setStars(float stars)
{ {
stars.Current = 0; starCounter.Current = stars;
starsLabel.Text = stars.Current.ToString("0.00"); starsLabel.Text = starCounter.Current.ToString("0.00");
});
} }
} }
} }

View File

@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
get get
{ {
foreach (var item in ScrollableContent) foreach (var item in Scroll.Children)
{ {
yield return item; yield return item;

View File

@ -12,7 +12,19 @@ using osuTK.Input;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
public class OsuScrollContainer : ScrollContainer<Drawable> public class OsuScrollContainer : OsuScrollContainer<Drawable>
{
public OsuScrollContainer()
{
}
public OsuScrollContainer(Direction direction)
: base(direction)
{
}
}
public class OsuScrollContainer<T> : ScrollContainer<T> where T : Drawable
{ {
public const float SCROLL_BAR_HEIGHT = 10; public const float SCROLL_BAR_HEIGHT = 10;
public const float SCROLL_BAR_PADDING = 3; public const float SCROLL_BAR_PADDING = 3;

View File

@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface
public override void DisplayAt(float scale) public override void DisplayAt(float scale)
{ {
scale = Math.Clamp(scale, min_star_scale, 1); scale = (float)Interpolation.Lerp(min_star_scale, 1, Math.Clamp(scale, 0, 1));
this.FadeTo(scale, fading_duration); this.FadeTo(scale, fading_duration);
Icon.ScaleTo(scale, scaling_duration, scaling_easing); Icon.ScaleTo(scale, scaling_duration, scaling_easing);

View File

@ -27,7 +27,6 @@ namespace osu.Game.Overlays.Settings.Sections
new AudioDevicesSettings(), new AudioDevicesSettings(),
new VolumeSettings(), new VolumeSettings(),
new OffsetSettings(), new OffsetSettings(),
new MainMenuSettings()
}; };
} }
} }

View File

@ -26,7 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[] Children = new Drawable[]
{ {
new GeneralSettings(), new GeneralSettings(),
new SongSelectSettings(),
new ModsSettings(), new ModsSettings(),
}; };
} }

View File

@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Settings.Sections
new RendererSettings(), new RendererSettings(),
new LayoutSettings(), new LayoutSettings(),
new DetailSettings(), new DetailSettings(),
new UserInterfaceSettings(),
}; };
} }
} }

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
{
/// <summary>
/// A slider intended to show a "size" multiplier number, where 1x is 1.0.
/// </summary>
internal class SizeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
}

View File

@ -54,12 +54,6 @@ namespace osu.Game.Overlays.Settings.Sections
skinDropdown = new SkinSettingsDropdown(), skinDropdown = new SkinSettingsDropdown(),
new ExportSkinButton(), new ExportSkinButton(),
new SettingsSlider<float, SizeSlider> new SettingsSlider<float, SizeSlider>
{
LabelText = "Menu cursor size",
Current = config.GetBindable<float>(OsuSetting.MenuCursorSize),
KeyboardStep = 0.01f
},
new SettingsSlider<float, SizeSlider>
{ {
LabelText = "Gameplay cursor size", LabelText = "Gameplay cursor size",
Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize), Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize),
@ -136,11 +130,6 @@ namespace osu.Game.Overlays.Settings.Sections
Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray());
} }
private class SizeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
private class SkinSettingsDropdown : SettingsDropdown<SkinInfo> private class SkinSettingsDropdown : SettingsDropdown<SkinInfo>
{ {
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl(); protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl();

View File

@ -6,11 +6,11 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Graphics namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class UserInterfaceSettings : SettingsSubsection public class GeneralSettings : SettingsSubsection
{ {
protected override string Header => "User Interface"; protected override string Header => "General";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
@ -22,6 +22,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
LabelText = "Rotate cursor when dragging", LabelText = "Rotate cursor when dragging",
Current = config.GetBindable<bool>(OsuSetting.CursorRotation) Current = config.GetBindable<bool>(OsuSetting.CursorRotation)
}, },
new SettingsSlider<float, SizeSlider>
{
LabelText = "Menu cursor size",
Current = config.GetBindable<float>(OsuSetting.MenuCursorSize),
KeyboardStep = 0.01f
},
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Parallax", LabelText = "Parallax",

View File

@ -7,7 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
namespace osu.Game.Overlays.Settings.Sections.Audio namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class MainMenuSettings : SettingsSubsection public class MainMenuSettings : SettingsSubsection
{ {

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Gameplay namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
public class SongSelectSettings : SettingsSubsection public class SongSelectSettings : SettingsSubsection
{ {

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Settings.Sections.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
{
public class UserInterfaceSection : SettingsSection
{
public override string Header => "User Interface";
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.LayerGroup
};
public UserInterfaceSection()
{
Children = new Drawable[]
{
new GeneralSettings(),
new MainMenuSettings(),
new SongSelectSettings()
};
}
}
}

View File

@ -23,10 +23,11 @@ namespace osu.Game.Overlays
{ {
new GeneralSection(), new GeneralSection(),
new GraphicsSection(), new GraphicsSection(),
new GameplaySection(),
new AudioSection(), new AudioSection(),
new SkinSection(),
new InputSection(createSubPanel(new KeyBindingPanel())), new InputSection(createSubPanel(new KeyBindingPanel())),
new UserInterfaceSection(),
new GameplaySection(),
new SkinSection(),
new OnlineSection(), new OnlineSection(),
new MaintenanceSection(), new MaintenanceSection(),
new DebugSection(), new DebugSection(),

View File

@ -32,9 +32,6 @@ namespace osu.Game.Rulesets.Judgements
private readonly Container aboveHitObjectsContent; private readonly Container aboveHitObjectsContent;
[Resolved]
private ISkinSource skinSource { get; set; }
/// <summary> /// <summary>
/// Duration of initial fade in. /// Duration of initial fade in.
/// </summary> /// </summary>
@ -78,29 +75,6 @@ namespace osu.Game.Rulesets.Judgements
public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy(); public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
protected override void LoadComplete()
{
base.LoadComplete();
skinSource.SourceChanged += onSkinChanged;
}
private void onSkinChanged()
{
// on a skin change, the child component will update but not get correctly triggered to play its animation.
// we need to trigger a reinitialisation to make things right.
currentDrawableType = null;
PrepareForUse();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skinSource != null)
skinSource.SourceChanged -= onSkinChanged;
}
/// <summary> /// <summary>
/// Apply top-level animations to the current judgement when successfully hit. /// Apply top-level animations to the current judgement when successfully hit.
/// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required. /// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required.
@ -142,13 +116,14 @@ namespace osu.Game.Rulesets.Judgements
Debug.Assert(Result != null); Debug.Assert(Result != null);
prepareDrawables();
runAnimation(); runAnimation();
} }
private void runAnimation() private void runAnimation()
{ {
// is a no-op if the drawables are already in a correct state.
prepareDrawables();
// undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state.
ApplyTransformsAt(double.MinValue, true); ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true); ClearTransforms(true);
@ -203,7 +178,6 @@ namespace osu.Game.Rulesets.Judgements
if (JudgementBody != null) if (JudgementBody != null)
RemoveInternal(JudgementBody); RemoveInternal(JudgementBody);
aboveHitObjectsContent.Clear();
AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ => AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ =>
CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
{ {
@ -211,14 +185,29 @@ namespace osu.Game.Rulesets.Judgements
Origin = Anchor.Centre, Origin = Anchor.Centre,
}); });
if (JudgementBody.Drawable is IAnimatableJudgement animatable) JudgementBody.OnSkinChanged += () =>
{ {
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); // on a skin change, the child component will update but not get correctly triggered to play its animation (or proxy the newly created content).
if (proxiedContent != null) // we need to trigger a reinitialisation to make things right.
aboveHitObjectsContent.Add(proxiedContent); proxyContent();
} runAnimation();
};
proxyContent();
currentDrawableType = type; currentDrawableType = type;
void proxyContent()
{
aboveHitObjectsContent.Clear();
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
{
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
if (proxiedContent != null)
aboveHitObjectsContent.Add(proxiedContent);
}
}
} }
protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result); protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result);

View File

@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
base.LoadComplete(); base.LoadComplete();
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); comboIndexBindable.BindValueChanged(_ => UpdateComboColour(), true);
updateState(ArmedState.Idle, true); updateState(ArmedState.Idle, true);
} }
@ -263,18 +263,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnApply(); OnApply();
HitObjectApplied?.Invoke(this); HitObjectApplied?.Invoke(this);
// If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. // If not loaded, the state update happens in LoadComplete().
if (IsLoaded) if (IsLoaded)
{ {
Scheduler.Add(() => if (Result.IsHit)
{ updateState(ArmedState.Hit, true);
if (Result.IsHit) else if (Result.HasResult)
updateState(ArmedState.Hit, true); updateState(ArmedState.Miss, true);
else if (Result.HasResult) else
updateState(ArmedState.Miss, true); updateState(ArmedState.Idle, true);
else
updateState(ArmedState.Idle, true);
});
} }
hasHitObjectApplied = true; hasHitObjectApplied = true;
@ -533,7 +530,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin, allowFallback);
updateComboColour(); UpdateComboColour();
ApplySkin(skin, allowFallback); ApplySkin(skin, allowFallback);
@ -541,7 +538,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(State.Value, true); updateState(State.Value, true);
} }
private void updateComboColour() protected void UpdateComboColour()
{ {
if (!(HitObject is IHasComboInformation combo)) return; if (!(HitObject is IHasComboInformation combo)) return;

View File

@ -496,10 +496,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime;
foreach (HitObject obj in Beatmap.SelectedHitObjects) foreach (HitObject obj in Beatmap.SelectedHitObjects)
{
obj.StartTime += offset; obj.StartTime += offset;
Beatmap.Update(obj);
}
} }
return true; return true;

View File

@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit
var newState = stream.ToArray(); var newState = stream.ToArray();
// if the previous state is binary equal we don't need to push a new one, unless this is the initial state. // if the previous state is binary equal we don't need to push a new one, unless this is the initial state.
if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; if (savedStates.Count > 0 && newState.SequenceEqual(savedStates[currentState])) return;
if (currentState < savedStates.Count - 1) if (currentState < savedStates.Count - 1)
savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1);

View File

@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select
/// </summary> /// </summary>
public bool BeatmapSetsLoaded { get; private set; } public bool BeatmapSetsLoaded { get; private set; }
private readonly CarouselScrollContainer scroll; protected readonly CarouselScrollContainer Scroll;
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>(); private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
@ -112,9 +112,9 @@ namespace osu.Game.Screens.Select
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
selectedBeatmapSet = null; selectedBeatmapSet = null;
ScrollableContent.Clear(false); Scroll.Clear(false);
itemsCache.Invalidate(); itemsCache.Invalidate();
scrollPositionCache.Invalidate(); ScrollToSelected();
// apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false). // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false).
FlushPendingFilterOperations(); FlushPendingFilterOperations();
@ -130,9 +130,7 @@ namespace osu.Game.Screens.Select
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>(); private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
private readonly Cached itemsCache = new Cached(); private readonly Cached itemsCache = new Cached();
private readonly Cached scrollPositionCache = new Cached(); private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None;
protected readonly Container<DrawableCarouselItem> ScrollableContent;
public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>(); public Bindable<bool> RightClickScrollingEnabled = new Bindable<bool>();
@ -155,17 +153,12 @@ namespace osu.Game.Screens.Select
InternalChild = new OsuContextMenuContainer InternalChild = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = scroll = new CarouselScrollContainer Children = new Drawable[]
{ {
Masking = false, setPool,
RelativeSizeAxes = Axes.Both, Scroll = new CarouselScrollContainer
Children = new Drawable[]
{ {
setPool, RelativeSizeAxes = Axes.Both,
ScrollableContent = new Container<DrawableCarouselItem>
{
RelativeSizeAxes = Axes.X,
}
} }
} }
}; };
@ -180,7 +173,7 @@ namespace osu.Game.Screens.Select
config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm);
config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled);
RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue;
RightClickScrollingEnabled.TriggerChange(); RightClickScrollingEnabled.TriggerChange();
itemUpdated = beatmaps.ItemUpdated.GetBoundCopy(); itemUpdated = beatmaps.ItemUpdated.GetBoundCopy();
@ -421,12 +414,12 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// The position of the lower visible bound with respect to the current scroll position. /// The position of the lower visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom; private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom;
/// <summary> /// <summary>
/// The position of the upper visible bound with respect to the current scroll position. /// The position of the upper visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleUpperBound => scroll.Current - BleedTop; private float visibleUpperBound => Scroll.Current - BleedTop;
public void FlushPendingFilterOperations() public void FlushPendingFilterOperations()
{ {
@ -468,8 +461,8 @@ namespace osu.Game.Screens.Select
root.Filter(activeCriteria); root.Filter(activeCriteria);
itemsCache.Invalidate(); itemsCache.Invalidate();
if (alwaysResetScrollPosition || !scroll.UserScrolling) if (alwaysResetScrollPosition || !Scroll.UserScrolling)
ScrollToSelected(); ScrollToSelected(true);
} }
} }
@ -478,7 +471,12 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// Scroll to the current <see cref="SelectedBeatmap"/>. /// Scroll to the current <see cref="SelectedBeatmap"/>.
/// </summary> /// </summary>
public void ScrollToSelected() => scrollPositionCache.Invalidate(); /// <param name="immediate">
/// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels.
/// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation.
/// </param>
public void ScrollToSelected(bool immediate = false) =>
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
#region Key / button selection logic #region Key / button selection logic
@ -488,12 +486,12 @@ namespace osu.Game.Screens.Select
{ {
case Key.Left: case Key.Left:
if (!e.Repeat) if (!e.Repeat)
beginRepeatSelection(() => SelectNext(-1, true), e.Key); beginRepeatSelection(() => SelectNext(-1), e.Key);
return true; return true;
case Key.Right: case Key.Right:
if (!e.Repeat) if (!e.Repeat)
beginRepeatSelection(() => SelectNext(1, true), e.Key); beginRepeatSelection(() => SelectNext(), e.Key);
return true; return true;
} }
@ -580,6 +578,11 @@ namespace osu.Game.Screens.Select
if (revalidateItems) if (revalidateItems)
updateYPositions(); updateYPositions();
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
if (pendingScrollOperation != PendingScrollOperation.None)
updateScrollPosition();
// This data is consumed to find the currently displayable range. // This data is consumed to find the currently displayable range.
// This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn. // This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn.
var newDisplayRange = getDisplayRange(); var newDisplayRange = getDisplayRange();
@ -594,7 +597,7 @@ namespace osu.Game.Screens.Select
{ {
var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1);
foreach (var panel in ScrollableContent.Children) foreach (var panel in Scroll.Children)
{ {
if (toDisplay.Remove(panel.Item)) if (toDisplay.Remove(panel.Item))
{ {
@ -620,24 +623,14 @@ namespace osu.Game.Screens.Select
panel.Depth = item.CarouselYPosition; panel.Depth = item.CarouselYPosition;
panel.Y = item.CarouselYPosition; panel.Y = item.CarouselYPosition;
ScrollableContent.Add(panel); Scroll.Add(panel);
} }
} }
} }
// Finally, if the filtered items have changed, animate drawables to their new locations.
// This is common if a selected/collapsed state has changed.
if (revalidateItems)
{
foreach (DrawableCarouselItem panel in ScrollableContent.Children)
{
panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint);
}
}
// Update externally controlled state of currently visible items (e.g. x-offset and opacity). // Update externally controlled state of currently visible items (e.g. x-offset and opacity).
// This is a per-frame update on all drawable panels. // This is a per-frame update on all drawable panels.
foreach (DrawableCarouselItem item in ScrollableContent.Children) foreach (DrawableCarouselItem item in Scroll.Children)
{ {
updateItem(item); updateItem(item);
@ -670,14 +663,6 @@ namespace osu.Game.Screens.Select
return (firstIndex, lastIndex); return (firstIndex, lastIndex);
} }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (!scrollPositionCache.IsValid)
updateScrollPosition();
}
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem) private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
{ {
if (weakItem.NewValue.TryGetTarget(out var item)) if (weakItem.NewValue.TryGetTarget(out var item))
@ -789,7 +774,8 @@ namespace osu.Game.Screens.Select
} }
currentY += visibleHalfHeight; currentY += visibleHalfHeight;
ScrollableContent.Height = currentY;
Scroll.ScrollContent.Height = currentY;
if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected))
{ {
@ -809,12 +795,31 @@ namespace osu.Game.Screens.Select
if (firstScroll) if (firstScroll)
{ {
// reduce movement when first displaying the carousel. // reduce movement when first displaying the carousel.
scroll.ScrollTo(scrollTarget.Value - 200, false); Scroll.ScrollTo(scrollTarget.Value - 200, false);
firstScroll = false; firstScroll = false;
} }
scroll.ScrollTo(scrollTarget.Value); switch (pendingScrollOperation)
scrollPositionCache.Validate(); {
case PendingScrollOperation.Standard:
Scroll.ScrollTo(scrollTarget.Value);
break;
case PendingScrollOperation.Immediate:
// in order to simplify animation logic, rather than using the animated version of ScrollTo,
// we take the difference in scroll height and apply to all visible panels.
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
// to enter clamp-special-case mode where it animates completely differently to normal.
float scrollChange = scrollTarget.Value - Scroll.Current;
Scroll.ScrollTo(scrollTarget.Value, false);
foreach (var i in Scroll.Children)
i.Y += scrollChange;
break;
}
pendingScrollOperation = PendingScrollOperation.None;
} }
} }
@ -844,7 +849,7 @@ namespace osu.Game.Screens.Select
/// <param name="parent">For nested items, the parent of the item to be updated.</param> /// <param name="parent">For nested items, the parent of the item to be updated.</param>
private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null)
{ {
Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre);
float itemDrawY = posInScroll.Y - visibleUpperBound; float itemDrawY = posInScroll.Y - visibleUpperBound;
float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight);
@ -858,6 +863,13 @@ namespace osu.Game.Screens.Select
item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
} }
private enum PendingScrollOperation
{
None,
Standard,
Immediate,
}
/// <summary> /// <summary>
/// A carousel item strictly used for binary search purposes. /// A carousel item strictly used for binary search purposes.
/// </summary> /// </summary>
@ -889,7 +901,7 @@ namespace osu.Game.Screens.Select
} }
} }
private class CarouselScrollContainer : OsuScrollContainer protected class CarouselScrollContainer : OsuScrollContainer<DrawableCarouselItem>
{ {
private bool rightMouseScrollBlocked; private bool rightMouseScrollBlocked;
@ -898,6 +910,12 @@ namespace osu.Game.Screens.Select
/// </summary> /// </summary>
public bool UserScrolling { get; private set; } public bool UserScrolling { get; private set; }
public CarouselScrollContainer()
{
// size is determined by the carousel itself, due to not all content necessarily being loaded.
ScrollContent.AutoSizeAxes = Axes.None;
}
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{ {

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -60,6 +61,25 @@ namespace osu.Game.Screens.Select.Carousel
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
} }
protected override void Update()
{
base.Update();
// position updates should not occur if the item is filtered away.
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
if (!Item.Visible)
return;
float targetY = Item.CarouselYPosition;
if (Precision.AlmostEquals(targetY, Y))
Y = targetY;
else
// algorithm for this is taken from ScrollContainer.
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
}
protected override void UpdateItem() protected override void UpdateItem()
{ {
base.UpdateItem(); base.UpdateItem();

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1120.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1127.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="Sentry" Version="2.1.6" /> <PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1120.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1127.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1120.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1127.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />