mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 11:12:54 +08:00
Merge branch 'master' into replay-pausing-support
This commit is contained in:
commit
220657c2cc
@ -38,17 +38,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
new Fruit
|
||||
{
|
||||
X = 0,
|
||||
StartTime = 250
|
||||
StartTime = 1000
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
X = CatchPlayfield.WIDTH,
|
||||
StartTime = 500
|
||||
StartTime = 2000
|
||||
},
|
||||
new JuiceStream
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
StartTime = 750,
|
||||
StartTime = 3000,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
hitObject.Scale = 1.5f;
|
||||
|
||||
if (hyperdash)
|
||||
hitObject.HyperDashTarget = new Banana();
|
||||
((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana();
|
||||
|
||||
d.Anchor = Anchor.Centre;
|
||||
d.RelativePositionAxes = Axes.None;
|
||||
|
@ -5,11 +5,11 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.MathUtils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
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.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
{
|
||||
@ -192,24 +192,24 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
private static void initialiseHyperDash(IBeatmap beatmap)
|
||||
{
|
||||
List<CatchHitObject> objectWithDroplets = new List<CatchHitObject>();
|
||||
List<PalpableCatchHitObject> palpableObjects = new List<PalpableCatchHitObject>();
|
||||
|
||||
foreach (var currentObject in beatmap.HitObjects)
|
||||
{
|
||||
if (currentObject is Fruit fruitObject)
|
||||
objectWithDroplets.Add(fruitObject);
|
||||
palpableObjects.Add(fruitObject);
|
||||
|
||||
if (currentObject is JuiceStream)
|
||||
{
|
||||
foreach (var currentJuiceElement in currentObject.NestedHitObjects)
|
||||
foreach (var juice in currentObject.NestedHitObjects)
|
||||
{
|
||||
if (!(currentJuiceElement is TinyDroplet))
|
||||
objectWithDroplets.Add((CatchHitObject)currentJuiceElement);
|
||||
if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet))
|
||||
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;
|
||||
|
||||
@ -221,10 +221,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
int lastDirection = 0;
|
||||
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];
|
||||
CatchHitObject nextObject = objectWithDroplets[i + 1];
|
||||
var currentObject = palpableObjects[i];
|
||||
var nextObject = palpableObjects[i + 1];
|
||||
|
||||
// Reset variables in-case values have changed (e.g. after applying HR)
|
||||
currentObject.HyperDashTarget = null;
|
||||
|
@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
{
|
||||
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 LastNormalizedPosition;
|
||||
|
@ -2,13 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
public class Banana : Fruit
|
||||
public class Banana : Fruit, IHasComboInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of banana in current shower.
|
||||
@ -26,6 +29,29 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
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 static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };
|
||||
|
@ -27,11 +27,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
set => x = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this object can be placed on the catcher's plate.
|
||||
/// </summary>
|
||||
public virtual bool CanBePlated => false;
|
||||
|
||||
/// <summary>
|
||||
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
@ -63,13 +58,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
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>();
|
||||
|
||||
/// <summary>
|
||||
@ -83,16 +71,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
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)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
@ -105,14 +83,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
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
|
||||
{
|
||||
Pear,
|
||||
|
@ -1,10 +1,8 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
@ -46,20 +36,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
if (Samples != null)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.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 Container bananaContainer;
|
||||
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
switch (hitObject)
|
||||
{
|
||||
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);
|
||||
|
@ -2,69 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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>
|
||||
{
|
||||
protected override double InitialLifetimeOffset => HitObject.TimePreempt;
|
||||
|
||||
public virtual bool StaysOnPlate => HitObject.CanBePlated;
|
||||
|
||||
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
|
||||
|
||||
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
|
||||
@ -73,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
: base(hitObject)
|
||||
{
|
||||
X = hitObject.X;
|
||||
Anchor = Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
public Func<CatchHitObject, bool> CheckPosition;
|
||||
|
@ -4,15 +4,16 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
{
|
||||
public class DrawableDroplet : PalpableDrawableCatchHitObject<Droplet>
|
||||
public class DrawableDroplet : DrawablePalpableCatchHitObject
|
||||
{
|
||||
public override bool StaysOnPlate => false;
|
||||
|
||||
public DrawableDroplet(Droplet h)
|
||||
public DrawableDroplet(CatchHitObject h)
|
||||
: base(h)
|
||||
{
|
||||
}
|
||||
|
@ -4,13 +4,14 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osuTK;
|
||||
|
||||
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 Container dropletContainer;
|
||||
@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
switch (hitObject)
|
||||
{
|
||||
case CatchHitObject catchObject:
|
||||
return createDrawableRepresentation?.Invoke(catchObject)?.With(o =>
|
||||
((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
|
||||
return createDrawableRepresentation?.Invoke(catchObject);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}.");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
|
||||
{
|
||||
public class BananaPiece : PulpFormation
|
||||
{
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,9 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
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
|
||||
{
|
||||
@ -19,8 +16,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
/// </summary>
|
||||
public const float RADIUS_ADJUST = 1.1f;
|
||||
|
||||
private Circle border;
|
||||
private CatchHitObject hitObject;
|
||||
private BorderPiece border;
|
||||
private PalpableCatchHitObject hitObject;
|
||||
|
||||
public FruitPiece()
|
||||
{
|
||||
@ -30,52 +27,18 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableObject)
|
||||
{
|
||||
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
|
||||
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
|
||||
hitObject = drawableCatchObject.HitObject;
|
||||
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
getFruitFor(drawableCatchObject.HitObject.VisualRepresentation),
|
||||
border = new Circle
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
getFruitFor(hitObject.VisualRepresentation),
|
||||
border = new BorderPiece(),
|
||||
});
|
||||
|
||||
if (hitObject.HyperDash)
|
||||
{
|
||||
AddInternal(new Circle
|
||||
{
|
||||
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,
|
||||
}
|
||||
}
|
||||
});
|
||||
AddInternal(new HyperBorderPiece());
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
|
||||
{
|
||||
public class GrapePiece : PulpFormation
|
||||
{
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
|
||||
{
|
||||
public class PearPiece : PulpFormation
|
||||
{
|
@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
|
||||
{
|
||||
public class PineapplePiece : PulpFormation
|
||||
{
|
@ -10,7 +10,7 @@ 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.Pieces
|
||||
{
|
||||
public abstract class PulpFormation : CompositeDrawable
|
||||
{
|
@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
|
||||
{
|
||||
public class RaspberryPiece : PulpFormation
|
||||
{
|
35
osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
Normal file
35
osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
Normal 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];
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
float lastPosition = CatchPlayfield.CENTER_X;
|
||||
double lastTime = 0;
|
||||
|
||||
void moveToNext(CatchHitObject h)
|
||||
void moveToNext(PalpableCatchHitObject h)
|
||||
{
|
||||
float positionChange = Math.Abs(lastPosition - h.X);
|
||||
double timeAvailable = h.StartTime - lastTime;
|
||||
@ -101,23 +101,16 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
foreach (var obj in Beatmap.HitObjects)
|
||||
{
|
||||
switch (obj)
|
||||
if (obj is PalpableCatchHitObject palpableObject)
|
||||
{
|
||||
case Fruit _:
|
||||
moveToNext(obj);
|
||||
break;
|
||||
moveToNext(palpableObject);
|
||||
}
|
||||
|
||||
foreach (var nestedObj in obj.NestedHitObjects.Cast<CatchHitObject>())
|
||||
{
|
||||
switch (nestedObj)
|
||||
if (nestedObj is PalpableCatchHitObject palpableNestedObject)
|
||||
{
|
||||
case Banana _:
|
||||
case TinyDroplet _:
|
||||
case Droplet _:
|
||||
case Fruit _:
|
||||
moveToNext(nestedObj);
|
||||
break;
|
||||
moveToNext(palpableNestedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableObject, ISkinSource skin)
|
||||
{
|
||||
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
|
||||
var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject;
|
||||
|
||||
accentColour.BindTo(drawableCatchObject.AccentColour);
|
||||
|
||||
|
@ -55,21 +55,18 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
HitObjectContainer,
|
||||
CatcherArea,
|
||||
};
|
||||
|
||||
NewResult += onNewResult;
|
||||
RevertResult += onRevertResult;
|
||||
}
|
||||
|
||||
public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
|
||||
|
||||
public override void Add(DrawableHitObject h)
|
||||
protected override void OnNewDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
h.OnNewResult += onNewResult;
|
||||
h.OnRevertResult += onRevertResult;
|
||||
|
||||
base.Add(h);
|
||||
|
||||
var fruit = (DrawableCatchHitObject)h;
|
||||
fruit.CheckPosition = CheckIfWeCanCatch;
|
||||
((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
|
||||
}
|
||||
|
||||
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
|
||||
|
||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
|
||||
|
||||
|
@ -220,11 +220,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <summary>
|
||||
/// Let the catcher attempt to catch a fruit.
|
||||
/// </summary>
|
||||
/// <param name="fruit">The fruit to catch.</param>
|
||||
/// <param name="hitObject">The fruit to catch.</param>
|
||||
/// <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;
|
||||
|
||||
var halfCatchWidth = catchWidth * 0.5f;
|
||||
|
@ -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())
|
||||
return;
|
||||
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
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.
|
||||
var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
|
||||
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
if (caughtFruit == null) return;
|
||||
|
||||
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.Anchor = Anchor.TopCentre;
|
||||
@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
runAfterLoaded(() => MovableCatcher.Explode(caughtFruit));
|
||||
}
|
||||
|
||||
if (fruit.HitObject.LastInCombo)
|
||||
if (hitObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
runAfterLoaded(() => MovableCatcher.Explode());
|
||||
@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
MovableCatcher.Drop();
|
||||
}
|
||||
|
||||
comboDisplay.OnNewResult(fruit, result);
|
||||
comboDisplay.OnNewResult(hitObject, result);
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
|
||||
|
@ -96,6 +96,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override float GetBeatSnapDistanceAt(double referenceTime)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
var first = (OsuHitObject)objects.First();
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +174,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
private class SnapProvider : IPositionSnapProvider
|
||||
{
|
||||
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
|
||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,9 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
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.UI;
|
||||
using osuTK;
|
||||
@ -26,8 +29,51 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
protected override GameplayCursorContainer CreateCursor() => null;
|
||||
|
||||
protected override DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null)
|
||||
=> new DrawableOsuEditPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize);
|
||||
protected override void OnNewDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
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.
|
||||
if (distanceSnapGrid == null)
|
||||
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
||||
|
@ -19,7 +19,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
|
||||
public class DrawableHitCircle : DrawableOsuHitObject
|
||||
{
|
||||
public OsuAction? HitAction => HitArea.HitAction;
|
||||
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
|
||||
public class DrawableSlider : DrawableOsuHitObject
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -25,8 +26,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
public class OsuPlayfield : Playfield
|
||||
{
|
||||
public readonly Func<DrawableHitObject, double, bool> CheckHittable;
|
||||
|
||||
private readonly PlayfieldBorder playfieldBorder;
|
||||
private readonly ProxyContainer approachCircles;
|
||||
private readonly ProxyContainer spinnerProxies;
|
||||
@ -56,7 +55,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
};
|
||||
|
||||
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
||||
CheckHittable = hitPolicy.IsHittable;
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
|
||||
@ -68,6 +66,29 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
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)
|
||||
{
|
||||
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
|
||||
@ -78,28 +99,19 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
|
||||
|
||||
registerPool<HitCircle, DrawableHitCircle>(10, 100);
|
||||
RegisterPool<HitCircle, DrawableHitCircle>(10, 100);
|
||||
|
||||
registerPool<Slider, DrawableSlider>(10, 100);
|
||||
registerPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
|
||||
registerPool<SliderTailCircle, DrawableSliderTail>(10, 100);
|
||||
registerPool<SliderTick, DrawableSliderTick>(10, 100);
|
||||
registerPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
|
||||
RegisterPool<Slider, DrawableSlider>(10, 100);
|
||||
RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
|
||||
RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100);
|
||||
RegisterPool<SliderTick, DrawableSliderTick>(10, 100);
|
||||
RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
|
||||
|
||||
registerPool<Spinner, DrawableSpinner>(2, 20);
|
||||
registerPool<SpinnerTick, DrawableSpinnerTick>(10, 100);
|
||||
registerPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100);
|
||||
RegisterPool<Spinner, DrawableSpinner>(2, 20);
|
||||
RegisterPool<SpinnerTick, DrawableSpinnerTick>(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 void OnHitObjectAdded(HitObject hitObject)
|
||||
@ -114,27 +126,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
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)
|
||||
{
|
||||
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
|
||||
|
@ -153,6 +153,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private class SnapProvider : IPositionSnapProvider
|
||||
{
|
||||
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
|
||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public float GetBeatSnapDistanceAt(double referenceTime) => 10;
|
||||
|
181
osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs
Normal file
181
osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs
Normal 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 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -196,7 +196,7 @@ namespace osu.Game.Configuration
|
||||
|
||||
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
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -14,6 +15,7 @@ namespace osu.Game.Graphics.Containers
|
||||
/// <summary>
|
||||
/// A container that can scroll to each section inside it.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public class SectionsContainer<T> : Container<T>
|
||||
where T : Drawable
|
||||
{
|
||||
|
@ -119,7 +119,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -164,10 +164,10 @@ namespace osu.Game.Input.Bindings
|
||||
[Description("Toggle now playing overlay")]
|
||||
ToggleNowPlaying,
|
||||
|
||||
[Description("Previous Selection")]
|
||||
[Description("Previous selection")]
|
||||
SelectPrevious,
|
||||
|
||||
[Description("Next Selection")]
|
||||
[Description("Next selection")]
|
||||
SelectNext,
|
||||
|
||||
[Description("Home")]
|
||||
@ -176,26 +176,26 @@ namespace osu.Game.Input.Bindings
|
||||
[Description("Toggle notifications")]
|
||||
ToggleNotifications,
|
||||
|
||||
[Description("Pause Gameplay")]
|
||||
[Description("Pause gameplay")]
|
||||
PauseGameplay,
|
||||
|
||||
// Editor
|
||||
[Description("Setup Mode")]
|
||||
[Description("Setup mode")]
|
||||
EditorSetupMode,
|
||||
|
||||
[Description("Compose Mode")]
|
||||
[Description("Compose mode")]
|
||||
EditorComposeMode,
|
||||
|
||||
[Description("Design Mode")]
|
||||
[Description("Design mode")]
|
||||
EditorDesignMode,
|
||||
|
||||
[Description("Timing Mode")]
|
||||
[Description("Timing mode")]
|
||||
EditorTimingMode,
|
||||
|
||||
[Description("Hold for HUD")]
|
||||
HoldForHUD,
|
||||
|
||||
[Description("Random Skin")]
|
||||
[Description("Random skin")]
|
||||
RandomSkin,
|
||||
|
||||
[Description("Pause / resume replay")]
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Overlays.OSD;
|
||||
|
||||
@ -37,11 +38,11 @@ namespace osu.Game.Overlays.Music
|
||||
bool wasPlaying = musicController.IsPlaying;
|
||||
|
||||
if (musicController.TogglePause())
|
||||
onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track"));
|
||||
onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", action));
|
||||
return true;
|
||||
|
||||
case GlobalAction.MusicNext:
|
||||
musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track")));
|
||||
musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", action)));
|
||||
|
||||
return true;
|
||||
|
||||
@ -51,11 +52,11 @@ namespace osu.Game.Overlays.Music
|
||||
switch (res)
|
||||
{
|
||||
case PreviousTrackResult.Restart:
|
||||
onScreenDisplay?.Display(new MusicActionToast("Restart track"));
|
||||
onScreenDisplay?.Display(new MusicActionToast("Restart track", action));
|
||||
break;
|
||||
|
||||
case PreviousTrackResult.Previous:
|
||||
onScreenDisplay?.Display(new MusicActionToast("Previous track"));
|
||||
onScreenDisplay?.Display(new MusicActionToast("Previous track", action));
|
||||
break;
|
||||
}
|
||||
});
|
||||
@ -72,9 +73,18 @@ namespace osu.Game.Overlays.Music
|
||||
|
||||
private class MusicActionToast : Toast
|
||||
{
|
||||
public MusicActionToast(string action)
|
||||
: base("Music Playback", action, string.Empty)
|
||||
private readonly GlobalAction action;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,13 @@ namespace osu.Game.Overlays.OSD
|
||||
private const int toast_minimum_width = 240;
|
||||
|
||||
private readonly Container content;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
protected readonly OsuSpriteText ValueText;
|
||||
|
||||
protected readonly OsuSpriteText ShortcutText;
|
||||
|
||||
protected Toast(string description, string value, string shortcut)
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
@ -68,7 +71,7 @@ namespace osu.Game.Overlays.OSD
|
||||
Origin = Anchor.Centre,
|
||||
Text = value
|
||||
},
|
||||
new OsuSpriteText
|
||||
ShortcutText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -18,8 +18,10 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new PlayHistorySubsection(User),
|
||||
new PaginatedMostPlayedBeatmapContainer(User),
|
||||
new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero),
|
||||
new ReplaysSubsection(User)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -442,6 +442,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
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 DurationToDistance(double referenceTime, double duration);
|
||||
|
@ -8,12 +8,22 @@ namespace osu.Game.Rulesets.Edit
|
||||
public interface IPositionSnapProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Given a position, find a valid time snap.
|
||||
/// Given a position, find a valid time and position snap.
|
||||
/// </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>
|
||||
/// <returns>The time and position post-snapping.</returns>
|
||||
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>
|
||||
/// Retrieves the distance between two points within a timing point that are one beat length apart.
|
||||
/// </summary>
|
||||
|
@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </summary>
|
||||
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>
|
||||
/// Whether a visual indicator should be displayed when a scoring result occurs.
|
||||
/// </summary>
|
||||
@ -141,6 +146,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
private Container<PausableSkinnableSound> samplesContainer;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the initialization logic in <see cref="Playfield" /> has applied.
|
||||
/// </summary>
|
||||
internal bool IsInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
@ -214,10 +224,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
foreach (var h in HitObject.NestedHitObjects)
|
||||
{
|
||||
var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h)
|
||||
var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h);
|
||||
var drawableNested = pooledDrawableNested
|
||||
?? CreateNestedHitObject(h)
|
||||
?? 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.OnRevertResult += onRevertResult;
|
||||
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
|
||||
@ -514,11 +529,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
private void updateComboColour()
|
||||
{
|
||||
if (!(HitObject is IHasComboInformation)) return;
|
||||
if (!(HitObject is IHasComboInformation combo)) return;
|
||||
|
||||
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value;
|
||||
|
||||
AccentColour.Value = GetComboColour(comboColours);
|
||||
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
|
||||
AccentColour.Value = combo.GetComboColour(comboColours);
|
||||
}
|
||||
|
||||
/// <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"/>.
|
||||
/// </remarks>
|
||||
/// <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)
|
||||
{
|
||||
if (!(HitObject is IHasComboInformation combo))
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
using System.Diagnostics;
|
||||
|
||||
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>
|
||||
/// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield.
|
||||
/// </summary>
|
||||
@ -124,6 +135,11 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <param name="h">The DrawableHitObject to add.</param>
|
||||
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);
|
||||
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>
|
||||
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
|
||||
/// </summary>
|
||||
@ -321,10 +348,12 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
var dho = (DrawableHitObject)d;
|
||||
|
||||
// If this is the first time this DHO is being used (not loaded), then apply the DHO mods.
|
||||
// This is done before Apply() so that the state is updated once when the hitobject is applied.
|
||||
if (!dho.IsLoaded)
|
||||
if (!dho.IsInitialized)
|
||||
{
|
||||
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>())
|
||||
m.ApplyToDrawableHitObjects(dho.Yield());
|
||||
}
|
||||
|
@ -187,7 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (e.Button == MouseButton.Right)
|
||||
return false;
|
||||
|
||||
if (movementBlueprint != null)
|
||||
if (movementBlueprints != null)
|
||||
{
|
||||
isDraggingBlueprint = true;
|
||||
changeHandler?.BeginChange();
|
||||
@ -299,7 +299,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
SelectionBlueprints.Remove(blueprint);
|
||||
|
||||
if (movementBlueprint == blueprint)
|
||||
if (movementBlueprints?.Contains(blueprint) == true)
|
||||
finishSelectionMovement();
|
||||
|
||||
OnBlueprintRemoved(hitObject);
|
||||
@ -424,8 +424,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
#region Selection Movement
|
||||
|
||||
private Vector2? movementBlueprintOriginalPosition;
|
||||
private SelectionBlueprint movementBlueprint;
|
||||
private Vector2[] movementBlueprintOriginalPositions;
|
||||
private SelectionBlueprint[] movementBlueprints;
|
||||
private bool isDraggingBlueprint;
|
||||
|
||||
/// <summary>
|
||||
@ -442,8 +442,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return;
|
||||
|
||||
// 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();
|
||||
movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct
|
||||
movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray();
|
||||
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -453,30 +453,47 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <returns>Whether a movement was active.</returns>
|
||||
private bool moveCurrentSelection(DragEvent e)
|
||||
{
|
||||
if (movementBlueprint == null)
|
||||
if (movementBlueprints == null)
|
||||
return false;
|
||||
|
||||
if (snapProvider == null)
|
||||
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.
|
||||
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
|
||||
|
||||
// Move the hitobjects.
|
||||
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
|
||||
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition)))
|
||||
return true;
|
||||
|
||||
if (result.Time.HasValue)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
@ -494,11 +511,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <returns>Whether a movement was active.</returns>
|
||||
private bool finishSelectionMovement()
|
||||
{
|
||||
if (movementBlueprint == null)
|
||||
if (movementBlueprints == null)
|
||||
return false;
|
||||
|
||||
movementBlueprintOriginalPosition = null;
|
||||
movementBlueprint = null;
|
||||
movementBlueprintOriginalPositions = null;
|
||||
movementBlueprints = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -224,6 +224,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
/// </summary>
|
||||
public double VisibleRange => track.Length / Zoom;
|
||||
|
||||
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
|
||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -19,8 +18,8 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -28,32 +27,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
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]
|
||||
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 List<Container> shadowComponents = new List<Container>();
|
||||
|
||||
private DrawableHitObject drawableHitObject;
|
||||
|
||||
private Bindable<Color4> comboColour;
|
||||
|
||||
private readonly Container mainComponents;
|
||||
|
||||
private readonly OsuSpriteText comboIndexText;
|
||||
|
||||
private Bindable<int> comboIndex;
|
||||
|
||||
private const float thickness = 5;
|
||||
|
||||
private const float shadow_radius = 5;
|
||||
|
||||
private const float circle_size = 24;
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
|
||||
public TimelineHitObjectBlueprint(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
@ -152,46 +145,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
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()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (HitObject is IHasComboInformation comboInfo)
|
||||
{
|
||||
comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
|
||||
comboIndex.BindValueChanged(combo =>
|
||||
{
|
||||
comboIndexText.Text = (combo.NewValue + 1).ToString();
|
||||
}, true);
|
||||
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
|
||||
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
|
||||
|
||||
comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy();
|
||||
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
|
||||
|
||||
skin.SourceChanged += updateComboColour;
|
||||
}
|
||||
}
|
||||
|
||||
if (drawableHitObject != null)
|
||||
{
|
||||
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;
|
||||
private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
|
||||
|
||||
var col = mainComponents.Colour.TopLeft.Linear;
|
||||
float brightness = col.R + col.G + col.B;
|
||||
private void updateComboColour()
|
||||
{
|
||||
if (!(HitObject is IHasComboInformation combo))
|
||||
return;
|
||||
|
||||
// decide the combo index colour based on brightness?
|
||||
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
|
||||
}, true);
|
||||
}
|
||||
var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
|
||||
var comboColour = combo.GetComboColour(comboColours);
|
||||
|
||||
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()
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -43,6 +44,21 @@ namespace osu.Game.Screens.Edit.Compose
|
||||
if (ruleset == null || composer == null)
|
||||
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);
|
||||
|
||||
// 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.
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -375,6 +375,9 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
|
||||
return false;
|
||||
|
||||
const double precision = 1;
|
||||
|
||||
double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y;
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
@ -21,6 +23,9 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
|
||||
private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>();
|
||||
|
||||
[Resolved]
|
||||
private SectionsContainer<SetupSection> sectionsContainer { get; set; }
|
||||
|
||||
public FileChooserLabelledTextBox()
|
||||
{
|
||||
currentFile.BindValueChanged(onFileSelected);
|
||||
@ -47,14 +52,16 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
|
||||
public void DisplayFileChooser()
|
||||
{
|
||||
Target.Child = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions)
|
||||
FileSelector fileSelector;
|
||||
|
||||
Target.Child = fileSelector = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 400,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
CurrentFile = { BindTarget = currentFile }
|
||||
};
|
||||
|
||||
sectionsContainer.ScrollTo(fileSelector);
|
||||
}
|
||||
|
||||
internal class FileChooserOsuTextBox : OsuTextBox
|
||||
|
Loading…
Reference in New Issue
Block a user