Merge branch 'master' into replay-pausing-support

This commit is contained in:
Dan Balasescu
2020-11-27 00:09:40 +09:00
committed by GitHub
66 changed files with 1213 additions and 541 deletions

View File

@ -38,17 +38,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
new Fruit new Fruit
{ {
X = 0, X = 0,
StartTime = 250 StartTime = 1000
}, },
new Fruit new Fruit
{ {
X = CatchPlayfield.WIDTH, X = CatchPlayfield.WIDTH,
StartTime = 500 StartTime = 2000
}, },
new JuiceStream new JuiceStream
{ {
X = CatchPlayfield.CENTER_X, X = CatchPlayfield.CENTER_X,
StartTime = 750, StartTime = 3000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
} }
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests
hitObject.Scale = 1.5f; hitObject.Scale = 1.5f;
if (hyperdash) if (hyperdash)
hitObject.HyperDashTarget = new Banana(); ((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana();
d.Anchor = Anchor.Centre; d.Anchor = Anchor.Centre;
d.RelativePositionAxes = Axes.None; d.RelativePositionAxes = Axes.None;

View File

@ -5,11 +5,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps namespace osu.Game.Rulesets.Catch.Beatmaps
{ {
@ -192,24 +192,24 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void initialiseHyperDash(IBeatmap beatmap) private static void initialiseHyperDash(IBeatmap beatmap)
{ {
List<CatchHitObject> objectWithDroplets = new List<CatchHitObject>(); List<PalpableCatchHitObject> palpableObjects = new List<PalpableCatchHitObject>();
foreach (var currentObject in beatmap.HitObjects) foreach (var currentObject in beatmap.HitObjects)
{ {
if (currentObject is Fruit fruitObject) if (currentObject is Fruit fruitObject)
objectWithDroplets.Add(fruitObject); palpableObjects.Add(fruitObject);
if (currentObject is JuiceStream) if (currentObject is JuiceStream)
{ {
foreach (var currentJuiceElement in currentObject.NestedHitObjects) foreach (var juice in currentObject.NestedHitObjects)
{ {
if (!(currentJuiceElement is TinyDroplet)) if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet))
objectWithDroplets.Add((CatchHitObject)currentJuiceElement); palpableObjects.Add(palpableObject);
} }
} }
} }
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
@ -221,10 +221,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
int lastDirection = 0; int lastDirection = 0;
double lastExcess = halfCatcherWidth; double lastExcess = halfCatcherWidth;
for (int i = 0; i < objectWithDroplets.Count - 1; i++) for (int i = 0; i < palpableObjects.Count - 1; i++)
{ {
CatchHitObject currentObject = objectWithDroplets[i]; var currentObject = palpableObjects[i];
CatchHitObject nextObject = objectWithDroplets[i + 1]; var nextObject = palpableObjects[i + 1];
// Reset variables in-case values have changed (e.g. after applying HR) // Reset variables in-case values have changed (e.g. after applying HR)
currentObject.HyperDashTarget = null; currentObject.HyperDashTarget = null;

View File

@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
{ {
private const float normalized_hitobject_radius = 41.0f; private const float normalized_hitobject_radius = 41.0f;
public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject; public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
public new CatchHitObject LastObject => (CatchHitObject)base.LastObject; public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
public readonly float NormalizedPosition; public readonly float NormalizedPosition;
public readonly float LastNormalizedPosition; public readonly float LastNormalizedPosition;

View File

@ -2,13 +2,16 @@
// 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.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class Banana : Fruit public class Banana : Fruit, IHasComboInformation
{ {
/// <summary> /// <summary>
/// Index of banana in current shower. /// Index of banana in current shower.
@ -26,6 +29,29 @@ namespace osu.Game.Rulesets.Catch.Objects
Samples = samples; Samples = samples;
} }
private Color4? colour;
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours)
{
// override any external colour changes with banananana
return colour ??= getBananaColour();
}
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
{
default:
return new Color4(255, 240, 0, 255);
case 1:
return new Color4(255, 192, 0, 255);
case 2:
return new Color4(214, 221, 28, 255);
}
}
private class BananaHitSampleInfo : HitSampleInfo private class BananaHitSampleInfo : HitSampleInfo
{ {
private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };

View File

@ -27,11 +27,6 @@ namespace osu.Game.Rulesets.Catch.Objects
set => x = value; set => x = value;
} }
/// <summary>
/// Whether this object can be placed on the catcher's plate.
/// </summary>
public virtual bool CanBePlated => false;
/// <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>
@ -63,13 +58,6 @@ namespace osu.Game.Rulesets.Catch.Objects
set => ComboIndexBindable.Value = value; set => ComboIndexBindable.Value = value;
} }
/// <summary>
/// Difference between the distance to the next object
/// and the distance that would have triggered a hyper dash.
/// A value close to 0 indicates a difficult jump (for difficulty calculation).
/// </summary>
public float DistanceToHyperDash { get; set; }
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
/// <summary> /// <summary>
@ -83,16 +71,6 @@ namespace osu.Game.Rulesets.Catch.Objects
public float Scale { get; set; } = 1; public float Scale { get; set; } = 1;
/// <summary>
/// Whether this fruit can initiate a hyperdash.
/// </summary>
public bool HyperDash => HyperDashTarget != null;
/// <summary>
/// The target fruit if we are to initiate a hyperdash.
/// </summary>
public CatchHitObject HyperDashTarget;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -105,14 +83,6 @@ namespace osu.Game.Rulesets.Catch.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
} }
/// <summary>
/// Represents a single object that can be caught by the catcher.
/// </summary>
public abstract class PalpableCatchHitObject : CatchHitObject
{
public override bool CanBePlated => true;
}
public enum FruitVisualRepresentation public enum FruitVisualRepresentation
{ {
Pear, Pear,

View File

@ -1,10 +1,8 @@
// 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.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
@ -15,14 +13,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
} }
private Color4? colour;
protected override Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
{
// override any external colour changes with banananana
return colour ??= getBananaColour();
}
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();
@ -46,20 +36,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
if (Samples != null) if (Samples != null)
Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f; Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
} }
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
{
default:
return new Color4(255, 240, 0, 255);
case 1:
return new Color4(255, 192, 0, 255);
case 2:
return new Color4(214, 221, 28, 255);
}
}
} }
} }

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableBananaShower : DrawableCatchHitObject<BananaShower> public class DrawableBananaShower : DrawableCatchHitObject
{ {
private readonly Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation; private readonly Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation;
private readonly Container bananaContainer; private readonly Container bananaContainer;
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
switch (hitObject) switch (hitObject)
{ {
case Banana banana: case Banana banana:
return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false); return createDrawableRepresentation?.Invoke(banana);
} }
return base.CreateNestedHitObject(hitObject); return base.CreateNestedHitObject(hitObject);

View File

@ -2,69 +2,16 @@
// 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 System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public abstract class PalpableDrawableCatchHitObject<TObject> : DrawableCatchHitObject<TObject>
where TObject : PalpableCatchHitObject
{
protected Container ScaleContainer { get; private set; }
protected PalpableDrawableCatchHitObject(TObject hitObject)
: base(hitObject)
{
Origin = Anchor.Centre;
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
Masking = false;
}
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
ScaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
}
});
ScaleContainer.Scale = new Vector2(HitObject.Scale);
}
protected override Color4 GetComboColour(IReadOnlyList<Color4> comboColours) =>
comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count];
}
public abstract class DrawableCatchHitObject<TObject> : DrawableCatchHitObject
where TObject : CatchHitObject
{
public new TObject HitObject;
protected DrawableCatchHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
Anchor = Anchor.BottomLeft;
}
}
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject> public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{ {
protected override double InitialLifetimeOffset => HitObject.TimePreempt; protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public virtual bool StaysOnPlate => HitObject.CanBePlated;
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;
@ -73,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
: base(hitObject) : base(hitObject)
{ {
X = hitObject.X; X = hitObject.X;
Anchor = Anchor.BottomLeft;
} }
public Func<CatchHitObject, bool> CheckPosition; public Func<CatchHitObject, bool> CheckPosition;

View File

@ -4,15 +4,16 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableDroplet : PalpableDrawableCatchHitObject<Droplet> public class DrawableDroplet : DrawablePalpableCatchHitObject
{ {
public override bool StaysOnPlate => false; public override bool StaysOnPlate => false;
public DrawableDroplet(Droplet h) public DrawableDroplet(CatchHitObject h)
: base(h) : base(h)
{ {
} }

View File

@ -4,13 +4,14 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : PalpableDrawableCatchHitObject<Fruit> public class DrawableFruit : DrawablePalpableCatchHitObject
{ {
public DrawableFruit(Fruit h) public DrawableFruit(CatchHitObject h)
: base(h) : base(h)
{ {
} }

View File

@ -10,7 +10,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableJuiceStream : DrawableCatchHitObject<JuiceStream> public class DrawableJuiceStream : DrawableCatchHitObject
{ {
private readonly Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation; private readonly Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation;
private readonly Container dropletContainer; private readonly Container dropletContainer;
@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
switch (hitObject) switch (hitObject)
{ {
case CatchHitObject catchObject: case CatchHitObject catchObject:
return createDrawableRepresentation?.Invoke(catchObject)?.With(o => return createDrawableRepresentation?.Invoke(catchObject);
((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
} }
throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}."); throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}.");

View File

@ -0,0 +1,42 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject
{
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
/// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
/// </summary>
public virtual bool StaysOnPlate => true;
protected readonly Container ScaleContainer;
protected DrawablePalpableCatchHitObject(CatchHitObject h)
: base(h)
{
Origin = Anchor.Centre;
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
AddInternal(ScaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
});
}
[BackgroundDependencyLoader]
private void load()
{
ScaleContainer.Scale = new Vector2(HitObject.Scale);
}
}
}

View File

@ -1,69 +0,0 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DropletPiece : CompositeDrawable
{
public DropletPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
var hitObject = drawableCatchObject.HitObject;
InternalChild = new Pulp
{
RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour }
};
if (hitObject.HyperDash)
{
AddInternal(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(2f),
Depth = 1,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 6,
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
}
}
});
}
}
}
}

View File

@ -2,10 +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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class BananaPiece : PulpFormation public class BananaPiece : PulpFormation
{ {

View File

@ -0,0 +1,31 @@
// 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.Shapes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
public class BorderPiece : Circle
{
public BorderPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
BorderColour = Color4.White;
BorderThickness = 6f * FruitPiece.RADIUS_ADJUST;
// Border is drawn only when there is a child drawable.
Child = new Box
{
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both,
};
}
}
}

View File

@ -0,0 +1,36 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
public class DropletPiece : CompositeDrawable
{
public DropletPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
InternalChild = new Pulp
{
RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour }
};
if (drawableCatchObject.HitObject.HyperDash)
{
AddInternal(new HyperDropletBorderPiece());
}
}
}
}

View File

@ -5,12 +5,9 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
internal class FruitPiece : CompositeDrawable internal class FruitPiece : CompositeDrawable
{ {
@ -19,8 +16,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
/// </summary> /// </summary>
public const float RADIUS_ADJUST = 1.1f; public const float RADIUS_ADJUST = 1.1f;
private Circle border; private BorderPiece border;
private CatchHitObject hitObject; private PalpableCatchHitObject hitObject;
public FruitPiece() public FruitPiece()
{ {
@ -30,52 +27,18 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject) private void load(DrawableHitObject drawableObject)
{ {
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
hitObject = drawableCatchObject.HitObject; hitObject = drawableCatchObject.HitObject;
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
getFruitFor(drawableCatchObject.HitObject.VisualRepresentation), getFruitFor(hitObject.VisualRepresentation),
border = new Circle border = new BorderPiece(),
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Color4.White,
BorderThickness = 6f * RADIUS_ADJUST,
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both
}
}
},
}); });
if (hitObject.HyperDash) if (hitObject.HyperDash)
{ {
AddInternal(new Circle AddInternal(new HyperBorderPiece());
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 12f * RADIUS_ADJUST,
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
});
} }
} }

View File

@ -2,10 +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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class GrapePiece : PulpFormation public class GrapePiece : PulpFormation
{ {

View File

@ -0,0 +1,22 @@
// 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.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
public class HyperBorderPiece : BorderPiece
{
public HyperBorderPiece()
{
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
BorderThickness = 12f * FruitPiece.RADIUS_ADJUST;
Child.Alpha = 0.3f;
Child.Blending = BlendingParameters.Additive;
Child.Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
}
}
}

View File

@ -0,0 +1,14 @@
// 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.
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
public class HyperDropletBorderPiece : HyperBorderPiece
{
public HyperDropletBorderPiece()
{
Size /= 2;
BorderThickness = 6f;
}
}
}

View File

@ -2,10 +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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class PearPiece : PulpFormation public class PearPiece : PulpFormation
{ {

View File

@ -2,10 +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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class PineapplePiece : PulpFormation public class PineapplePiece : PulpFormation
{ {

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public abstract class PulpFormation : CompositeDrawable public abstract class PulpFormation : CompositeDrawable
{ {

View File

@ -2,10 +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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class RaspberryPiece : PulpFormation public class RaspberryPiece : PulpFormation
{ {

View File

@ -0,0 +1,35 @@
// 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 System.Collections.Generic;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects
{
/// <summary>
/// Represents a single object that can be caught by the catcher.
/// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects.
/// </summary>
public abstract class PalpableCatchHitObject : CatchHitObject, IHasComboInformation
{
/// <summary>
/// Difference between the distance to the next object
/// and the distance that would have triggered a hyper dash.
/// A value close to 0 indicates a difficult jump (for difficulty calculation).
/// </summary>
public float DistanceToHyperDash { get; set; }
/// <summary>
/// Whether this fruit can initiate a hyperdash.
/// </summary>
public bool HyperDash => HyperDashTarget != null;
/// <summary>
/// The target fruit if we are to initiate a hyperdash.
/// </summary>
public CatchHitObject HyperDashTarget;
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays
float lastPosition = CatchPlayfield.CENTER_X; float lastPosition = CatchPlayfield.CENTER_X;
double lastTime = 0; double lastTime = 0;
void moveToNext(CatchHitObject h) void moveToNext(PalpableCatchHitObject h)
{ {
float positionChange = Math.Abs(lastPosition - h.X); float positionChange = Math.Abs(lastPosition - h.X);
double timeAvailable = h.StartTime - lastTime; double timeAvailable = h.StartTime - lastTime;
@ -101,23 +101,16 @@ namespace osu.Game.Rulesets.Catch.Replays
foreach (var obj in Beatmap.HitObjects) foreach (var obj in Beatmap.HitObjects)
{ {
switch (obj) if (obj is PalpableCatchHitObject palpableObject)
{ {
case Fruit _: moveToNext(palpableObject);
moveToNext(obj);
break;
} }
foreach (var nestedObj in obj.NestedHitObjects.Cast<CatchHitObject>()) foreach (var nestedObj in obj.NestedHitObjects.Cast<CatchHitObject>())
{ {
switch (nestedObj) if (nestedObj is PalpableCatchHitObject palpableNestedObject)
{ {
case Banana _: moveToNext(palpableNestedObject);
case TinyDroplet _:
case Droplet _:
case Fruit _:
moveToNext(nestedObj);
break;
} }
} }
} }

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin) private void load(DrawableHitObject drawableObject, ISkinSource skin)
{ {
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
accentColour.BindTo(drawableCatchObject.AccentColour); accentColour.BindTo(drawableCatchObject.AccentColour);

View File

@ -55,21 +55,18 @@ namespace osu.Game.Rulesets.Catch.UI
HitObjectContainer, HitObjectContainer,
CatcherArea, CatcherArea,
}; };
NewResult += onNewResult;
RevertResult += onRevertResult;
} }
public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); protected override void OnNewDrawableHitObject(DrawableHitObject d)
public override void Add(DrawableHitObject h)
{ {
h.OnNewResult += onNewResult; ((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
h.OnRevertResult += onRevertResult;
base.Add(h);
var fruit = (DrawableCatchHitObject)h;
fruit.CheckPosition = CheckIfWeCanCatch;
} }
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);

View File

@ -220,11 +220,11 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// Let the catcher attempt to catch a fruit. /// Let the catcher attempt to catch a fruit.
/// </summary> /// </summary>
/// <param name="fruit">The fruit to catch.</param> /// <param name="hitObject">The fruit to catch.</param>
/// <returns>Whether the catch is possible.</returns> /// <returns>Whether the catch is possible.</returns>
public bool AttemptCatch(CatchHitObject fruit) public bool AttemptCatch(CatchHitObject hitObject)
{ {
if (!fruit.CanBePlated) if (!(hitObject is PalpableCatchHitObject fruit))
return false; return false;
var halfCatchWidth = catchWidth * 0.5f; var halfCatchWidth = catchWidth * 0.5f;

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.UI
}; };
} }
public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
{ {
if (!result.Type.IsScorable()) if (!result.Type.IsScorable())
return; return;
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
lastPlateableFruit.OnLoadComplete += _ => action(); lastPlateableFruit.OnLoadComplete += _ => action();
} }
if (result.IsHit && fruit.HitObject.CanBePlated) if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit)
{ {
// create a new (cloned) fruit to stay on the plate. the original is faded out immediately. // create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (caughtFruit == null) return; if (caughtFruit == null) return;
caughtFruit.RelativePositionAxes = Axes.None; caughtFruit.RelativePositionAxes = Axes.None;
caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(hitObject.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0);
caughtFruit.IsOnPlate = true; caughtFruit.IsOnPlate = true;
caughtFruit.Anchor = Anchor.TopCentre; caughtFruit.Anchor = Anchor.TopCentre;
@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.UI
runAfterLoaded(() => MovableCatcher.Explode(caughtFruit)); runAfterLoaded(() => MovableCatcher.Explode(caughtFruit));
} }
if (fruit.HitObject.LastInCombo) if (hitObject.HitObject.LastInCombo)
{ {
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
runAfterLoaded(() => MovableCatcher.Explode()); runAfterLoaded(() => MovableCatcher.Explode());
@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.Drop(); MovableCatcher.Drop();
} }
comboDisplay.OnNewResult(fruit, result); comboDisplay.OnNewResult(hitObject, result);
} }
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)

View File

@ -96,6 +96,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition)
{
throw new System.NotImplementedException();
}
public override float GetBeatSnapDistanceAt(double referenceTime) public override float GetBeatSnapDistanceAt(double referenceTime)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
var first = (OsuHitObject)objects.First(); var first = (OsuHitObject)objects.First();
var second = (OsuHitObject)objects.Last(); var second = (OsuHitObject)objects.Last();
return first.Position == second.Position; return Precision.AlmostEquals(first.EndPosition, second.Position);
}); });
} }
@ -86,5 +86,64 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return Precision.AlmostEquals(first.EndPosition, second.Position); return Precision.AlmostEquals(first.EndPosition, second.Position);
}); });
} }
[Test]
public void TestSecondCircleInSelectionAlsoSnaps()
{
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
AddStep("disable distance snap", () => InputManager.Key(Key.Q));
AddStep("enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("place first object", () => InputManager.Click(MouseButton.Left));
AddStep("increment time", () => EditorClock.SeekForward(true));
AddStep("move mouse right", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.2f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
AddStep("increment time", () => EditorClock.SeekForward(true));
AddStep("move mouse down", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Width * 0.2f)));
AddStep("place third object", () => InputManager.Click(MouseButton.Left));
AddStep("enter selection mode", () => InputManager.Key(Key.Number1));
AddStep("select objects 2 and 3", () =>
{
// add selection backwards to test non-sequential time ordering
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[2]);
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]);
});
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
AddAssert("object 3 snapped to 1", () =>
{
var objects = EditorBeatmap.HitObjects;
var first = (OsuHitObject)objects.First();
var third = (OsuHitObject)objects.Last();
return Precision.AlmostEquals(first.EndPosition, third.Position);
});
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f)));
AddAssert("object 2 snapped to 1", () =>
{
var objects = EditorBeatmap.HitObjects;
var first = (OsuHitObject)objects.First();
var second = (OsuHitObject)objects.ElementAt(1);
return Precision.AlmostEquals(first.EndPosition, second.Position);
});
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
}
} }
} }

View File

@ -174,6 +174,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private class SnapProvider : IPositionSnapProvider private class SnapProvider : IPositionSnapProvider
{ {
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;

View File

@ -1,63 +0,0 @@
// 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 System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditPool<T> : DrawableOsuPool<T>
where T : DrawableHitObject, new()
{
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
public DrawableOsuEditPool(Func<DrawableHitObject, double, bool> checkHittable, Action<Drawable> onLoaded, int initialSize, int? maximumSize = null)
: base(checkHittable, onLoaded, initialSize, maximumSize)
{
}
protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState);
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{
default:
// there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
}
}

View File

@ -2,9 +2,12 @@
// 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.Graphics.Pooling; using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
@ -26,8 +29,51 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
protected override GameplayCursorContainer CreateCursor() => null; protected override GameplayCursorContainer CreateCursor() => null;
protected override DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null) protected override void OnNewDrawableHitObject(DrawableHitObject d)
=> new DrawableOsuEditPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); {
d.ApplyCustomUpdateState += updateState;
}
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{
default:
// there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
} }
} }
} }

View File

@ -105,11 +105,20 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition)
{ {
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return snapResult; return snapResult;
return new SnapResult(screenSpacePosition, null);
}
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition);
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
return positionSnap;
// will be null if distance snap is disabled or not feasible for the current time value. // will be null if distance snap is disabled or not feasible for the current time value.
if (distanceSnapGrid == null) if (distanceSnapGrid == null)
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);

View File

@ -19,7 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach public class DrawableHitCircle : DrawableOsuHitObject
{ {
public OsuAction? HitAction => HitArea.HitAction; public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;

View File

@ -1,32 +0,0 @@
// 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 System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuPool<T> : DrawablePool<T>
where T : DrawableHitObject, new()
{
private readonly Func<DrawableHitObject, double, bool> checkHittable;
private readonly Action<Drawable> onLoaded;
public DrawableOsuPool(Func<DrawableHitObject, double, bool> checkHittable, Action<Drawable> onLoaded, int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
this.checkHittable = checkHittable;
this.onLoaded = onLoaded;
}
protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o =>
{
var osuObject = (DrawableOsuHitObject)(object)o;
osuObject.CheckHittable = checkHittable;
osuObject.OnLoadComplete += onLoaded;
});
}
}

View File

@ -19,7 +19,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach public class DrawableSlider : DrawableOsuHitObject
{ {
public new Slider HitObject => (Slider)base.HitObject; public new Slider HitObject => (Slider)base.HitObject;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -25,8 +26,6 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
public class OsuPlayfield : Playfield public class OsuPlayfield : Playfield
{ {
public readonly Func<DrawableHitObject, double, bool> CheckHittable;
private readonly PlayfieldBorder playfieldBorder; private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles; private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies; private readonly ProxyContainer spinnerProxies;
@ -56,7 +55,6 @@ namespace osu.Game.Rulesets.Osu.UI
}; };
hitPolicy = new OrderedHitPolicy(HitObjectContainer); hitPolicy = new OrderedHitPolicy(HitObjectContainer);
CheckHittable = hitPolicy.IsHittable;
var hitWindows = new OsuHitWindows(); var hitWindows = new OsuHitWindows();
@ -68,6 +66,29 @@ namespace osu.Game.Rulesets.Osu.UI
NewResult += onNewResult; NewResult += onNewResult;
} }
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
}
private void onDrawableHitObjectLoaded(Drawable drawable)
{
// note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded.
switch (drawable)
{
case DrawableSpinner _:
spinnerProxies.Add(drawable.CreateProxy());
break;
case DrawableHitCircle hitCircle:
approachCircles.Add(hitCircle.ProxiedLayer.CreateProxy());
break;
}
}
private void onJudgmentLoaded(DrawableOsuJudgement judgement) private void onJudgmentLoaded(DrawableOsuJudgement judgement)
{ {
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent()); judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
@ -78,28 +99,19 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
registerPool<HitCircle, DrawableHitCircle>(10, 100); RegisterPool<HitCircle, DrawableHitCircle>(10, 100);
registerPool<Slider, DrawableSlider>(10, 100); RegisterPool<Slider, DrawableSlider>(10, 100);
registerPool<SliderHeadCircle, DrawableSliderHead>(10, 100); RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
registerPool<SliderTailCircle, DrawableSliderTail>(10, 100); RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100);
registerPool<SliderTick, DrawableSliderTick>(10, 100); RegisterPool<SliderTick, DrawableSliderTick>(10, 100);
registerPool<SliderRepeat, DrawableSliderRepeat>(5, 50); RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
registerPool<Spinner, DrawableSpinner>(2, 20); RegisterPool<Spinner, DrawableSpinner>(2, 20);
registerPool<SpinnerTick, DrawableSpinnerTick>(10, 100); RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 100);
registerPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100); RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100);
} }
private void registerPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(CreatePool<TDrawable>(initialSize, maximumSize));
protected virtual DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null)
where TDrawable : DrawableHitObject, new()
=> new DrawableOsuPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize);
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
protected override void OnHitObjectAdded(HitObject hitObject) protected override void OnHitObjectAdded(HitObject hitObject)
@ -114,27 +126,6 @@ namespace osu.Game.Rulesets.Osu.UI
followPoints.RemoveFollowPoints((OsuHitObject)hitObject); followPoints.RemoveFollowPoints((OsuHitObject)hitObject);
} }
public void OnHitObjectLoaded(Drawable drawable)
{
switch (drawable)
{
case DrawableSliderHead _:
case DrawableSliderTail _:
case DrawableSliderTick _:
case DrawableSliderRepeat _:
case DrawableSpinnerTick _:
break;
case DrawableSpinner _:
spinnerProxies.Add(drawable.CreateProxy());
break;
case IDrawableHitObjectWithProxiedApproach approach:
approachCircles.Add(approach.ProxiedLayer.CreateProxy());
break;
}
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.

View File

@ -153,6 +153,9 @@ namespace osu.Game.Tests.Visual.Editing
private class SnapProvider : IPositionSnapProvider private class SnapProvider : IPositionSnapProvider
{ {
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => 10; public float GetBeatSnapDistanceAt(double referenceTime) => 10;

View File

@ -0,0 +1,181 @@
// 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.Overlays.Profile.Sections.Historical;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Users;
using NUnit.Framework;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using System;
using System.Linq;
using osu.Framework.Testing;
using osu.Framework.Graphics.Shapes;
using static osu.Game.Users.User;
namespace osu.Game.Tests.Visual.Online
{
public class TestScenePlayHistorySubsection : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
private readonly Bindable<User> user = new Bindable<User>();
private readonly PlayHistorySubsection section;
public TestScenePlayHistorySubsection()
{
AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
section = new PlayHistorySubsection(user)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}
});
}
[Test]
public void TestNullValues()
{
AddStep("Load user", () => user.Value = user_with_null_values);
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestEmptyValues()
{
AddStep("Load user", () => user.Value = user_with_empty_values);
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestOneValue()
{
AddStep("Load user", () => user.Value = user_with_one_value);
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestTwoValues()
{
AddStep("Load user", () => user.Value = user_with_two_values);
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestConstantValues()
{
AddStep("Load user", () => user.Value = user_with_constant_values);
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestConstantZeroValues()
{
AddStep("Load user", () => user.Value = user_with_zero_values);
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestFilledValues()
{
AddStep("Load user", () => user.Value = user_with_filled_values);
AddAssert("Section is visible", () => section.Alpha == 1);
AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlaycounts.Length == getChartValuesLength());
}
[Test]
public void TestMissingValues()
{
AddStep("Load user", () => user.Value = user_with_missing_values);
AddAssert("Section is visible", () => section.Alpha == 1);
AddAssert("Array length is 7", () => getChartValuesLength() == 7);
}
private int getChartValuesLength() => this.ChildrenOfType<ProfileLineChart>().Single().Values.Length;
private static readonly User user_with_null_values = new User
{
Id = 1
};
private static readonly User user_with_empty_values = new User
{
Id = 2,
MonthlyPlaycounts = Array.Empty<UserHistoryCount>()
};
private static readonly User user_with_one_value = new User
{
Id = 3,
MonthlyPlaycounts = new[]
{
new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 100 }
}
};
private static readonly User user_with_two_values = new User
{
Id = 4,
MonthlyPlaycounts = new[]
{
new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1 },
new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 2 }
}
};
private static readonly User user_with_constant_values = new User
{
Id = 5,
MonthlyPlaycounts = new[]
{
new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 5 },
new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 5 },
new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 5 }
}
};
private static readonly User user_with_zero_values = new User
{
Id = 6,
MonthlyPlaycounts = new[]
{
new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 0 },
new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 0 },
new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 0 }
}
};
private static readonly User user_with_filled_values = new User
{
Id = 7,
MonthlyPlaycounts = new[]
{
new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 },
new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 },
new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 },
new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 },
new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 },
new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 },
new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 }
}
};
private static readonly User user_with_missing_values = new User
{
Id = 8,
MonthlyPlaycounts = new[]
{
new UserHistoryCount { Date = new DateTime(2020, 1, 1), Count = 100 },
new UserHistoryCount { Date = new DateTime(2020, 7, 1), Count = 200 }
}
};
}
}

View File

@ -196,7 +196,7 @@ namespace osu.Game.Configuration
public Func<int, string> LookupSkinName { private get; set; } public Func<int, string> LookupSkinName { private get; set; }
public Func<GlobalAction, string> LookupKeyBindings { private get; set; } public Func<GlobalAction, string> LookupKeyBindings { get; set; }
} }
public enum OsuSetting public enum OsuSetting

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -14,6 +15,7 @@ namespace osu.Game.Graphics.Containers
/// <summary> /// <summary>
/// A container that can scroll to each section inside it. /// A container that can scroll to each section inside it.
/// </summary> /// </summary>
[Cached]
public class SectionsContainer<T> : Container<T> public class SectionsContainer<T> : Container<T>
where T : Drawable where T : Drawable
{ {

View File

@ -119,7 +119,11 @@ namespace osu.Game.Graphics.UserInterface
protected float GetYPosition(float value) protected float GetYPosition(float value)
{ {
if (ActualMaxValue == ActualMinValue) return 0; if (ActualMaxValue == ActualMinValue)
// show line at top if the only value on the graph is positive,
// and at bottom if the only value on the graph is zero or negative.
// just kind of makes most sense intuitively.
return value > 1 ? 0 : 1;
return (ActualMaxValue - value) / (ActualMaxValue - ActualMinValue); return (ActualMaxValue - value) / (ActualMaxValue - ActualMinValue);
} }

View File

@ -164,10 +164,10 @@ namespace osu.Game.Input.Bindings
[Description("Toggle now playing overlay")] [Description("Toggle now playing overlay")]
ToggleNowPlaying, ToggleNowPlaying,
[Description("Previous Selection")] [Description("Previous selection")]
SelectPrevious, SelectPrevious,
[Description("Next Selection")] [Description("Next selection")]
SelectNext, SelectNext,
[Description("Home")] [Description("Home")]
@ -176,26 +176,26 @@ namespace osu.Game.Input.Bindings
[Description("Toggle notifications")] [Description("Toggle notifications")]
ToggleNotifications, ToggleNotifications,
[Description("Pause Gameplay")] [Description("Pause gameplay")]
PauseGameplay, PauseGameplay,
// Editor // Editor
[Description("Setup Mode")] [Description("Setup mode")]
EditorSetupMode, EditorSetupMode,
[Description("Compose Mode")] [Description("Compose mode")]
EditorComposeMode, EditorComposeMode,
[Description("Design Mode")] [Description("Design mode")]
EditorDesignMode, EditorDesignMode,
[Description("Timing Mode")] [Description("Timing mode")]
EditorTimingMode, EditorTimingMode,
[Description("Hold for HUD")] [Description("Hold for HUD")]
HoldForHUD, HoldForHUD,
[Description("Random Skin")] [Description("Random skin")]
RandomSkin, RandomSkin,
[Description("Pause / resume replay")] [Description("Pause / resume replay")]

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays.OSD; using osu.Game.Overlays.OSD;
@ -37,11 +38,11 @@ namespace osu.Game.Overlays.Music
bool wasPlaying = musicController.IsPlaying; bool wasPlaying = musicController.IsPlaying;
if (musicController.TogglePause()) if (musicController.TogglePause())
onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track")); onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", action));
return true; return true;
case GlobalAction.MusicNext: case GlobalAction.MusicNext:
musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track"))); musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", action)));
return true; return true;
@ -51,11 +52,11 @@ namespace osu.Game.Overlays.Music
switch (res) switch (res)
{ {
case PreviousTrackResult.Restart: case PreviousTrackResult.Restart:
onScreenDisplay?.Display(new MusicActionToast("Restart track")); onScreenDisplay?.Display(new MusicActionToast("Restart track", action));
break; break;
case PreviousTrackResult.Previous: case PreviousTrackResult.Previous:
onScreenDisplay?.Display(new MusicActionToast("Previous track")); onScreenDisplay?.Display(new MusicActionToast("Previous track", action));
break; break;
} }
}); });
@ -72,9 +73,18 @@ namespace osu.Game.Overlays.Music
private class MusicActionToast : Toast private class MusicActionToast : Toast
{ {
public MusicActionToast(string action) private readonly GlobalAction action;
: base("Music Playback", action, string.Empty)
public MusicActionToast(string value, GlobalAction action)
: base("Music Playback", value, string.Empty)
{ {
this.action = action;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
ShortcutText.Text = config.LookupKeyBindings(action).ToUpperInvariant();
} }
} }
} }

View File

@ -16,10 +16,13 @@ namespace osu.Game.Overlays.OSD
private const int toast_minimum_width = 240; private const int toast_minimum_width = 240;
private readonly Container content; private readonly Container content;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
protected readonly OsuSpriteText ValueText; protected readonly OsuSpriteText ValueText;
protected readonly OsuSpriteText ShortcutText;
protected Toast(string description, string value, string shortcut) protected Toast(string description, string value, string shortcut)
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
@ -68,7 +71,7 @@ namespace osu.Game.Overlays.OSD
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = value Text = value
}, },
new OsuSpriteText ShortcutText = new OsuSpriteText
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,

View File

@ -0,0 +1,84 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public abstract class ChartProfileSubsection : ProfileSubsection
{
private ProfileLineChart chart;
protected ChartProfileSubsection(Bindable<User> user, string headerText)
: base(user, headerText)
{
}
protected override Drawable CreateContent() => new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Top = 10,
Left = 20,
Right = 40
},
Child = chart = new ProfileLineChart()
};
protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(onUserChanged, true);
}
private void onUserChanged(ValueChangedEvent<User> e)
{
var values = GetValues(e.NewValue);
if (values == null || values.Length <= 1)
{
Hide();
return;
}
chart.Values = fillZeroValues(values);
Show();
}
/// <summary>
/// Add entries for any missing months (filled with zero values).
/// </summary>
private UserHistoryCount[] fillZeroValues(UserHistoryCount[] historyEntries)
{
var filledHistoryEntries = new List<UserHistoryCount>();
foreach (var entry in historyEntries)
{
var lastFilled = filledHistoryEntries.LastOrDefault();
while (lastFilled?.Date.AddMonths(1) < entry.Date)
{
filledHistoryEntries.Add(lastFilled = new UserHistoryCount
{
Count = 0,
Date = lastFilled.Date.AddMonths(1)
});
}
filledHistoryEntries.Add(entry);
}
return filledHistoryEntries.ToArray();
}
protected abstract UserHistoryCount[] GetValues(User user);
}
}

View File

@ -0,0 +1,19 @@
// 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.Users;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class PlayHistorySubsection : ChartProfileSubsection
{
public PlayHistorySubsection(Bindable<User> user)
: base(user, "Play History")
{
}
protected override UserHistoryCount[] GetValues(User user) => user?.MonthlyPlaycounts;
}
}

View File

@ -0,0 +1,259 @@
// 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.Containers;
using osu.Framework.Graphics;
using JetBrains.Annotations;
using System;
using System.Linq;
using osu.Game.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class ProfileLineChart : CompositeDrawable
{
private UserHistoryCount[] values;
[NotNull]
public UserHistoryCount[] Values
{
get => values;
set
{
if (value.Length == 0)
throw new ArgumentException("At least one value expected!", nameof(value));
graph.Values = values = value;
createRowTicks();
createColumnTicks();
}
}
private readonly UserHistoryGraph graph;
private readonly Container<TickText> rowTicksContainer;
private readonly Container<TickText> columnTicksContainer;
private readonly Container<TickLine> rowLinesContainer;
private readonly Container<TickLine> columnLinesContainer;
public ProfileLineChart()
{
RelativeSizeAxes = Axes.X;
Height = 250;
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
rowTicksContainer = new Container<TickText>
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
},
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
rowLinesContainer = new Container<TickLine>
{
RelativeSizeAxes = Axes.Both
},
columnLinesContainer = new Container<TickLine>
{
RelativeSizeAxes = Axes.Both
}
}
},
graph = new UserHistoryGraph
{
RelativeSizeAxes = Axes.Both
}
}
}
},
new[]
{
Empty(),
columnTicksContainer = new Container<TickText>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Top = 10 }
}
}
}
};
}
private void createRowTicks()
{
rowTicksContainer.Clear();
rowLinesContainer.Clear();
var min = values.Select(v => v.Count).Min();
var max = values.Select(v => v.Count).Max();
var tickInterval = getTickInterval(max - min, 6);
for (long currentTick = 0; currentTick <= max; currentTick += tickInterval)
{
if (currentTick < min)
continue;
float y;
// special-case the min == max case to match LineGraph.
// lerp isn't really well-defined over a zero interval anyway.
if (min == max)
y = currentTick > 1 ? 1 : 0;
else
y = Interpolation.ValueAt(currentTick, 0, 1f, min, max);
// y axis is inverted in graph-like coordinates.
addRowTick(-y, currentTick);
}
}
private void createColumnTicks()
{
columnTicksContainer.Clear();
columnLinesContainer.Clear();
var totalMonths = values.Length;
int monthsPerTick = 1;
if (totalMonths > 80)
monthsPerTick = 12;
else if (totalMonths >= 45)
monthsPerTick = 3;
else if (totalMonths > 20)
monthsPerTick = 2;
for (int i = 0; i < totalMonths; i += monthsPerTick)
{
var x = (float)i / (totalMonths - 1);
addColumnTick(x, values[i].Date);
}
}
private void addRowTick(float y, double value)
{
rowTicksContainer.Add(new TickText
{
Anchor = Anchor.BottomRight,
Origin = Anchor.CentreRight,
RelativePositionAxes = Axes.Y,
Margin = new MarginPadding { Right = 3 },
Text = value.ToString("N0"),
Font = OsuFont.GetFont(size: 12),
Y = y
});
rowLinesContainer.Add(new TickLine
{
Anchor = Anchor.BottomRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
RelativePositionAxes = Axes.Y,
Height = 0.1f,
EdgeSmoothness = Vector2.One,
Y = y
});
}
private void addColumnTick(float x, DateTime value)
{
columnTicksContainer.Add(new TickText
{
Origin = Anchor.CentreLeft,
RelativePositionAxes = Axes.X,
Text = value.ToString("MMM yyyy"),
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Rotation = 45,
X = x
});
columnLinesContainer.Add(new TickLine
{
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
Width = 0.1f,
EdgeSmoothness = Vector2.One,
X = x
});
}
private long getTickInterval(long range, int maxTicksCount)
{
// this interval is what would be achieved if the interval was divided perfectly evenly into maxTicksCount ticks.
// can contain ugly fractional parts.
var exactTickInterval = (float)range / (maxTicksCount - 1);
// the ideal ticks start with a 1, 2 or 5, and are multipliers of powers of 10.
// first off, use log10 to calculate the number of digits in the "exact" interval.
var numberOfDigits = Math.Floor(Math.Log10(exactTickInterval));
var tickBase = Math.Pow(10, numberOfDigits);
// then see how the exact tick relates to the power of 10.
var exactTickMultiplier = exactTickInterval / tickBase;
double tickMultiplier;
// round up the fraction to start with a 1, 2 or 5. closest match wins.
if (exactTickMultiplier < 1.5)
tickMultiplier = 1.0;
else if (exactTickMultiplier < 3)
tickMultiplier = 2.0;
else if (exactTickMultiplier < 7)
tickMultiplier = 5.0;
else
tickMultiplier = 10.0;
return Math.Max((long)(tickMultiplier * tickBase), 1);
}
private class TickText : OsuSpriteText
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Foreground1;
}
}
private class TickLine : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background6;
}
}
}
}

View File

@ -0,0 +1,19 @@
// 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.Users;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class ReplaysSubsection : ChartProfileSubsection
{
public ReplaysSubsection(Bindable<User> user)
: base(user, "Replays Watched History")
{
}
protected override UserHistoryCount[] GetValues(User user) => user?.ReplaysWatchedCounts;
}
}

View File

@ -18,8 +18,10 @@ namespace osu.Game.Overlays.Profile.Sections
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new PlayHistorySubsection(User),
new PaginatedMostPlayedBeatmapContainer(User), new PaginatedMostPlayedBeatmapContainer(User),
new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero), new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero),
new ReplaysSubsection(User)
}; };
} }
} }

View File

@ -442,6 +442,9 @@ namespace osu.Game.Rulesets.Edit
public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float GetBeatSnapDistanceAt(double referenceTime);
public abstract float DurationToDistance(double referenceTime, double duration); public abstract float DurationToDistance(double referenceTime, double duration);

View File

@ -8,12 +8,22 @@ namespace osu.Game.Rulesets.Edit
public interface IPositionSnapProvider public interface IPositionSnapProvider
{ {
/// <summary> /// <summary>
/// Given a position, find a valid time snap. /// Given a position, find a valid time and position snap.
/// </summary> /// </summary>
/// <remarks>
/// This call should be equivalent to running <see cref="SnapScreenSpacePositionToValidPosition"/> with any additional logic that can be performed without the time immutability restriction.
/// </remarks>
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param> /// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
/// <returns>The time and position post-snapping.</returns> /// <returns>The time and position post-snapping.</returns>
SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
/// <summary>
/// Given a position, find a value position snap, restricting time to its input value.
/// </summary>
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
/// <returns>The position post-snapping. Time will always be null.</returns>
SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition);
/// <summary> /// <summary>
/// Retrieves the distance between two points within a timing point that are one beat length apart. /// Retrieves the distance between two points within a timing point that are one beat length apart.
/// </summary> /// </summary>

View File

@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary> /// </summary>
public event Action<DrawableHitObject, JudgementResult> OnRevertResult; public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
/// <summary>
/// Invoked when a new nested hit object is created by <see cref="CreateNestedHitObject" />.
/// </summary>
internal event Action<DrawableHitObject> OnNestedDrawableCreated;
/// <summary> /// <summary>
/// Whether a visual indicator should be displayed when a scoring result occurs. /// Whether a visual indicator should be displayed when a scoring result occurs.
/// </summary> /// </summary>
@ -141,6 +146,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
private Container<PausableSkinnableSound> samplesContainer; private Container<PausableSkinnableSound> samplesContainer;
/// <summary>
/// Whether the initialization logic in <see cref="Playfield" /> has applied.
/// </summary>
internal bool IsInitialized;
/// <summary> /// <summary>
/// Creates a new <see cref="DrawableHitObject"/>. /// Creates a new <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
@ -214,10 +224,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var h in HitObject.NestedHitObjects) foreach (var h in HitObject.NestedHitObjects)
{ {
var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h) var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h);
var drawableNested = pooledDrawableNested
?? CreateNestedHitObject(h) ?? CreateNestedHitObject(h)
?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
// Invoke the event only if this nested object is just created by `CreateNestedHitObject`.
if (pooledDrawableNested == null)
OnNestedDrawableCreated?.Invoke(drawableNested);
drawableNested.OnNewResult += onNewResult; drawableNested.OnNewResult += onNewResult;
drawableNested.OnRevertResult += onRevertResult; drawableNested.OnRevertResult += onRevertResult;
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
@ -514,11 +529,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
private void updateComboColour() private void updateComboColour()
{ {
if (!(HitObject is IHasComboInformation)) return; if (!(HitObject is IHasComboInformation combo)) return;
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value; var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
AccentColour.Value = combo.GetComboColour(comboColours);
AccentColour.Value = GetComboColour(comboColours);
} }
/// <summary> /// <summary>
@ -529,6 +543,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>. /// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>.
/// </remarks> /// </remarks>
/// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param> /// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param>
[Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527
protected virtual Color4 GetComboColour(IReadOnlyList<Color4> comboColours) protected virtual Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
{ {
if (!(HitObject is IHasComboInformation combo)) if (!(HitObject is IHasComboInformation combo))

View File

@ -1,12 +0,0 @@
// 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;
namespace osu.Game.Rulesets.Objects.Drawables
{
public interface IDrawableHitObjectWithProxiedApproach
{
Drawable ProxiedLayer { get; }
}
}

View File

@ -1,7 +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.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Types namespace osu.Game.Rulesets.Objects.Types
{ {
@ -35,5 +38,13 @@ namespace osu.Game.Rulesets.Objects.Types
/// Whether this is the last object in the current combo. /// Whether this is the last object in the current combo.
/// </summary> /// </summary>
bool LastInCombo { get; set; } bool LastInCombo { get; set; }
/// <summary>
/// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object from a set of possible combo colours.
/// Defaults to using <see cref="ComboIndex"/> to decide the colour.
/// </summary>
/// <param name="comboColours">A list of possible combo colours provided by the beatmap or skin.</param>
/// <returns>The colour of the combo described by this <see cref="IHasComboInformation"/> object.</returns>
Color4 GetComboColour([NotNull] IReadOnlyList<Color4> comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White;
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
using System.Diagnostics;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
@ -113,6 +114,16 @@ namespace osu.Game.Rulesets.UI
} }
} }
private void onNewDrawableHitObject(DrawableHitObject d)
{
d.OnNestedDrawableCreated += onNewDrawableHitObject;
OnNewDrawableHitObject(d);
Debug.Assert(!d.IsInitialized);
d.IsInitialized = true;
}
/// <summary> /// <summary>
/// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield.
/// </summary> /// </summary>
@ -124,6 +135,11 @@ namespace osu.Game.Rulesets.UI
/// <param name="h">The DrawableHitObject to add.</param> /// <param name="h">The DrawableHitObject to add.</param>
public virtual void Add(DrawableHitObject h) public virtual void Add(DrawableHitObject h)
{ {
if (h.IsInitialized)
throw new InvalidOperationException($"{nameof(Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead.");
onNewDrawableHitObject(h);
HitObjectContainer.Add(h); HitObjectContainer.Add(h);
OnHitObjectAdded(h.HitObject); OnHitObjectAdded(h.HitObject);
} }
@ -157,6 +173,17 @@ namespace osu.Game.Rulesets.UI
{ {
} }
/// <summary>
/// Invoked before a new <see cref="DrawableHitObject"/> is added to this <see cref="Playfield"/>.
/// It is invoked only once even if the drawable is pooled and used multiple times for different <see cref="HitObject"/>s.
/// </summary>
/// <remarks>
/// This is also invoked for nested <see cref="DrawableHitObject"/>s.
/// </remarks>
protected virtual void OnNewDrawableHitObject(DrawableHitObject drawableHitObject)
{
}
/// <summary> /// <summary>
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided. /// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
/// </summary> /// </summary>
@ -321,10 +348,12 @@ namespace osu.Game.Rulesets.UI
{ {
var dho = (DrawableHitObject)d; var dho = (DrawableHitObject)d;
// If this is the first time this DHO is being used (not loaded), then apply the DHO mods. if (!dho.IsInitialized)
// This is done before Apply() so that the state is updated once when the hitobject is applied.
if (!dho.IsLoaded)
{ {
onNewDrawableHitObject(dho);
// If this is the first time this DHO is being used, then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>()) foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
m.ApplyToDrawableHitObjects(dho.Yield()); m.ApplyToDrawableHitObjects(dho.Yield());
} }

View File

@ -187,7 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.Button == MouseButton.Right) if (e.Button == MouseButton.Right)
return false; return false;
if (movementBlueprint != null) if (movementBlueprints != null)
{ {
isDraggingBlueprint = true; isDraggingBlueprint = true;
changeHandler?.BeginChange(); changeHandler?.BeginChange();
@ -299,7 +299,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBlueprints.Remove(blueprint); SelectionBlueprints.Remove(blueprint);
if (movementBlueprint == blueprint) if (movementBlueprints?.Contains(blueprint) == true)
finishSelectionMovement(); finishSelectionMovement();
OnBlueprintRemoved(hitObject); OnBlueprintRemoved(hitObject);
@ -424,8 +424,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement #region Selection Movement
private Vector2? movementBlueprintOriginalPosition; private Vector2[] movementBlueprintOriginalPositions;
private SelectionBlueprint movementBlueprint; private SelectionBlueprint[] movementBlueprints;
private bool isDraggingBlueprint; private bool isDraggingBlueprint;
/// <summary> /// <summary>
@ -442,8 +442,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
return; return;
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprint = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray();
movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
} }
/// <summary> /// <summary>
@ -453,30 +453,47 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether a movement was active.</returns> /// <returns>Whether a movement was active.</returns>
private bool moveCurrentSelection(DragEvent e) private bool moveCurrentSelection(DragEvent e)
{ {
if (movementBlueprint == null) if (movementBlueprints == null)
return false; return false;
if (snapProvider == null) if (snapProvider == null)
return true; return true;
Debug.Assert(movementBlueprintOriginalPosition != null); Debug.Assert(movementBlueprintOriginalPositions != null);
HitObject draggedObject = movementBlueprint.HitObject; Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// check for positional snap for every object in selection (for things like object-object snapping)
for (var i = 0; i < movementBlueprintOriginalPositions.Length; i++)
{
var testPosition = movementBlueprintOriginalPositions[i] + distanceTravelled;
var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], positionalResult.ScreenSpacePosition)))
return true;
}
// if no positional snapping could be performed, try unrestricted snapping from the earliest
// hitobject in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition. // The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
// Retrieve a snapped position. // Retrieve a snapped position.
var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects. // Move the hitobjects.
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition)))
return true; return true;
if (result.Time.HasValue) if (result.Time.HasValue)
{ {
// Apply the start time at the newly snapped-to position // Apply the start time at the newly snapped-to position
double offset = result.Time.Value - draggedObject.StartTime; double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime;
foreach (HitObject obj in Beatmap.SelectedHitObjects) foreach (HitObject obj in Beatmap.SelectedHitObjects)
{ {
@ -494,11 +511,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether a movement was active.</returns> /// <returns>Whether a movement was active.</returns>
private bool finishSelectionMovement() private bool finishSelectionMovement()
{ {
if (movementBlueprint == null) if (movementBlueprints == null)
return false; return false;
movementBlueprintOriginalPosition = null; movementBlueprintOriginalPositions = null;
movementBlueprint = null; movementBlueprints = null;
return true; return true;
} }

View File

@ -224,6 +224,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary> /// </summary>
public double VisibleRange => track.Length / Zoom; public double VisibleRange => track.Length / Zoom;
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -19,8 +18,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -28,32 +27,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineHitObjectBlueprint : SelectionBlueprint public class TimelineHitObjectBlueprint : SelectionBlueprint
{ {
private readonly Circle circle; private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 24;
public Action<DragEvent> OnDragHandled;
[UsedImplicitly] [UsedImplicitly]
private readonly Bindable<double> startTime; private readonly Bindable<double> startTime;
public Action<DragEvent> OnDragHandled; private Bindable<int> indexInCurrentComboBindable;
private Bindable<int> comboIndexBindable;
private readonly Circle circle;
private readonly DragBar dragBar; private readonly DragBar dragBar;
private readonly List<Container> shadowComponents = new List<Container>(); private readonly List<Container> shadowComponents = new List<Container>();
private DrawableHitObject drawableHitObject;
private Bindable<Color4> comboColour;
private readonly Container mainComponents; private readonly Container mainComponents;
private readonly OsuSpriteText comboIndexText; private readonly OsuSpriteText comboIndexText;
private Bindable<int> comboIndex; [Resolved]
private ISkinSource skin { get; set; }
private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 24;
public TimelineHitObjectBlueprint(HitObject hitObject) public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject) : base(hitObject)
@ -152,46 +145,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
updateShadows(); updateShadows();
} }
[BackgroundDependencyLoader(true)]
private void load(HitObjectComposer composer)
{
if (composer != null)
{
// best effort to get the drawable representation for grabbing colour and what not.
drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject);
}
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
if (HitObject is IHasComboInformation comboInfo) if (HitObject is IHasComboInformation comboInfo)
{ {
comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
comboIndex.BindValueChanged(combo => indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
{
comboIndexText.Text = (combo.NewValue + 1).ToString(); comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
}, true); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
skin.SourceChanged += updateComboColour;
} }
}
if (drawableHitObject != null) private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
{
comboColour = drawableHitObject.AccentColour.GetBoundCopy();
comboColour.BindValueChanged(colour =>
{
if (HitObject is IHasDuration)
mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White);
else
mainComponents.Colour = drawableHitObject.AccentColour.Value;
var col = mainComponents.Colour.TopLeft.Linear; private void updateComboColour()
float brightness = col.R + col.G + col.B; {
if (!(HitObject is IHasComboInformation combo))
return;
// decide the combo index colour based on brightness? var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; var comboColour = combo.GetComboColour(comboColours);
}, true);
} if (HitObject is IHasDuration)
mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White);
else
mainComponents.Colour = comboColour;
var col = mainComponents.Colour.TopLeft.Linear;
float brightness = col.R + col.G + col.B;
// decide the combo index colour based on brightness?
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
} }
protected override void Update() protected override void Update()

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.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -43,6 +44,21 @@ namespace osu.Game.Screens.Edit.Compose
if (ruleset == null || composer == null) if (ruleset == null || composer == null)
return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer");
return wrapSkinnableContent(composer);
}
protected override Drawable CreateTimelineContent()
{
if (ruleset == null || composer == null)
return base.CreateTimelineContent();
return wrapSkinnableContent(new TimelineBlueprintContainer(composer));
}
private Drawable wrapSkinnableContent(Drawable content)
{
Debug.Assert(ruleset != null);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
@ -51,9 +67,7 @@ namespace osu.Game.Screens.Edit.Compose
// load the skinning hierarchy first. // load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
} }
protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer);
} }
} }

View File

@ -375,6 +375,9 @@ namespace osu.Game.Screens.Edit
protected override bool OnScroll(ScrollEvent e) protected override bool OnScroll(ScrollEvent e)
{ {
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
return false;
const double precision = 1; const double precision = 1;
double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y; double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y;

View File

@ -3,10 +3,12 @@
using System; using System;
using System.IO; using System.IO;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
@ -21,6 +23,9 @@ namespace osu.Game.Screens.Edit.Setup
private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>(); private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>();
[Resolved]
private SectionsContainer<SetupSection> sectionsContainer { get; set; }
public FileChooserLabelledTextBox() public FileChooserLabelledTextBox()
{ {
currentFile.BindValueChanged(onFileSelected); currentFile.BindValueChanged(onFileSelected);
@ -47,14 +52,16 @@ namespace osu.Game.Screens.Edit.Setup
public void DisplayFileChooser() public void DisplayFileChooser()
{ {
Target.Child = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) FileSelector fileSelector;
Target.Child = fileSelector = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 400, Height = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
CurrentFile = { BindTarget = currentFile } CurrentFile = { BindTarget = currentFile }
}; };
sectionsContainer.ScrollTo(fileSelector);
} }
internal class FileChooserOsuTextBox : OsuTextBox internal class FileChooserOsuTextBox : OsuTextBox