1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 15:12:57 +08:00

Merge branch 'master' into hit-sample-pooling

This commit is contained in:
smoogipoo 2020-11-30 18:31:58 +09:00
commit 4bbeb9ebd4
154 changed files with 3046 additions and 1294 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModEasy : ModEasy public class CatchModEasy : ModEasyWithExtraLives
{ {
public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
} }

View File

@ -3,21 +3,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class Banana : Fruit public class Banana : Fruit, IHasComboInformation
{ {
/// <summary> /// <summary>
/// Index of banana in current shower. /// Index of banana in current shower.
/// </summary> /// </summary>
public int BananaIndex; public int BananaIndex;
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement(); public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }; private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() };
@ -27,6 +28,29 @@ namespace osu.Game.Rulesets.Catch.Objects
Samples = samples; Samples = samples;
} }
private Color4? colour;
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours)
{
// override any external colour changes with banananana
return colour ??= getBananaColour();
}
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
{
default:
return new Color4(255, 240, 0, 255);
case 1:
return new Color4(255, 192, 0, 255);
case 2:
return new Color4(214, 221, 28, 255);
}
}
private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo> private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
{ {
private static readonly string[] lookup_names = { "metronomelow", "catch-banana" }; private static readonly string[] lookup_names = { "metronomelow", "catch-banana" };

View File

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

View File

@ -16,32 +16,47 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;
private float x; // This value is after XOffset applied.
public readonly Bindable<float> XBindable = new Bindable<float>();
// This value is before XOffset applied.
private float originalX;
/// <summary> /// <summary>
/// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>. /// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary> /// </summary>
public float X public float X
{ {
get => x + XOffset; // TODO: I don't like this asymmetry.
set => x = value; get => XBindable.Value;
// originalX is set by `XBindable.BindValueChanged`
set => XBindable.Value = value + xOffset;
} }
/// <summary> private float xOffset;
/// Whether this object can be placed on the catcher's plate.
/// </summary>
public virtual bool CanBePlated => false;
/// <summary> /// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>. /// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// </summary> /// </summary>
internal float XOffset { get; set; } internal float XOffset
{
get => xOffset;
set
{
xOffset = value;
XBindable.Value = originalX + xOffset;
}
}
public double TimePreempt = 1000; public double TimePreempt = 1000;
public int IndexInBeatmap { get; set; } public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>();
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); public int IndexInBeatmap
{
get => IndexInBeatmapBindable.Value;
set => IndexInBeatmapBindable.Value = value;
}
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
@ -63,13 +78,6 @@ namespace osu.Game.Rulesets.Catch.Objects
set => ComboIndexBindable.Value = value; set => ComboIndexBindable.Value = value;
} }
/// <summary>
/// Difference between the distance to the next object
/// and the distance that would have triggered a hyper dash.
/// A value close to 0 indicates a difficult jump (for difficulty calculation).
/// </summary>
public float DistanceToHyperDash { get; set; }
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>();
/// <summary> /// <summary>
@ -81,17 +89,13 @@ namespace osu.Game.Rulesets.Catch.Objects
set => LastInComboBindable.Value = value; set => LastInComboBindable.Value = value;
} }
public float Scale { get; set; } = 1; public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
/// <summary> public float Scale
/// Whether this fruit can initiate a hyperdash. {
/// </summary> get => ScaleBindable.Value;
public bool HyperDash => HyperDashTarget != null; set => ScaleBindable.Value = value;
}
/// <summary>
/// The target fruit if we are to initiate a hyperdash.
/// </summary>
public CatchHitObject HyperDashTarget;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
@ -103,22 +107,10 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
/// <summary> protected CatchHitObject()
/// Represents a single object that can be caught by the catcher. {
/// </summary> XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset);
public abstract class PalpableCatchHitObject : CatchHitObject }
{
public override bool CanBePlated => true;
}
public enum FruitVisualRepresentation
{
Pear,
Grape,
Pineapple,
Raspberry,
Banana // banananananannaanana
} }
} }

View File

@ -1,28 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableBanana : DrawableFruit public class DrawableBanana : DrawableFruit
{ {
protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana;
public DrawableBanana(Banana h) public DrawableBanana(Banana h)
: base(h) : base(h)
{ {
} }
private Color4? colour;
protected override Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
{
// override any external colour changes with banananana
return colour ??= getBananaColour();
}
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();
@ -46,20 +38,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
if (Samples != null) if (Samples != null)
Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f; Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
} }
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
{
default:
return new Color4(255, 240, 0, 255);
case 1:
return new Color4(255, 192, 0, 255);
case 2:
return new Color4(214, 221, 28, 255);
}
}
} }
} }

View File

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

View File

@ -2,77 +2,42 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public abstract class PalpableDrawableCatchHitObject<TObject> : DrawableCatchHitObject<TObject>
where TObject : PalpableCatchHitObject
{
protected Container ScaleContainer { get; private set; }
protected PalpableDrawableCatchHitObject(TObject hitObject)
: base(hitObject)
{
Origin = Anchor.Centre;
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
Masking = false;
}
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
ScaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
}
});
ScaleContainer.Scale = new Vector2(HitObject.Scale);
}
protected override Color4 GetComboColour(IReadOnlyList<Color4> comboColours) =>
comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count];
}
public abstract class DrawableCatchHitObject<TObject> : DrawableCatchHitObject
where TObject : CatchHitObject
{
public new TObject HitObject;
protected DrawableCatchHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
Anchor = Anchor.BottomLeft;
}
}
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject> public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{ {
protected override double InitialLifetimeOffset => HitObject.TimePreempt; public readonly Bindable<float> XBindable = new Bindable<float>();
public virtual bool StaysOnPlate => HitObject.CanBePlated; protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected DrawableCatchHitObject(CatchHitObject hitObject) protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
X = hitObject.X; Anchor = Anchor.BottomLeft;
}
protected override void OnApply()
{
base.OnApply();
XBindable.BindTo(HitObject.XBindable);
}
protected override void OnFree()
{
base.OnFree();
XBindable.UnbindFrom(HitObject.XBindable);
} }
public Func<CatchHitObject, bool> CheckPosition; public Func<CatchHitObject, bool> CheckPosition;

View File

@ -4,15 +4,16 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableDroplet : PalpableDrawableCatchHitObject<Droplet> public class DrawableDroplet : DrawablePalpableCatchHitObject
{ {
public override bool StaysOnPlate => false; public override bool StaysOnPlate => false;
public DrawableDroplet(Droplet h) public DrawableDroplet(CatchHitObject h)
: base(h) : base(h)
{ {
} }
@ -20,7 +21,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece()); HyperDash.BindValueChanged(_ => updatePiece(), true);
}
private void updatePiece()
{
ScaleContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Droplet),
_ => new DropletPiece
{
HyperDash = { BindTarget = HyperDash }
});
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

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

View File

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

View File

@ -0,0 +1,83 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
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;
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1);
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>();
/// <summary>
/// The multiplicative factor applied to <see cref="ScaleContainer"/> scale relative to <see cref="HitObject"/> scale.
/// </summary>
protected virtual float ScaleFactor => 1;
/// <summary>
/// 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([CanBeNull] 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()
{
XBindable.BindValueChanged(x =>
{
if (!IsOnPlate) X = x.NewValue;
}, true);
ScaleBindable.BindValueChanged(scale =>
{
ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
}, true);
IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());
}
protected override void OnApply()
{
base.OnApply();
HyperDash.BindTo(HitObject.HyperDashBindable);
ScaleBindable.BindTo(HitObject.ScaleBindable);
IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable);
}
protected override void OnFree()
{
HyperDash.UnbindFrom(HitObject.HyperDashBindable);
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable);
base.OnFree();
}
}
}

View File

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

View File

@ -1,69 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DropletPiece : CompositeDrawable
{
public DropletPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject;
var hitObject = drawableCatchObject.HitObject;
InternalChild = new Pulp
{
RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour }
};
if (hitObject.HyperDash)
{
AddInternal(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(2f),
Depth = 1,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 6,
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
}
}
});
}
}
}
}

View File

@ -1,111 +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.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
{
internal class FruitPiece : CompositeDrawable
{
/// <summary>
/// Because we're adding a border around the fruit, we need to scale down some.
/// </summary>
public const float RADIUS_ADJUST = 1.1f;
private Circle border;
private CatchHitObject hitObject;
public FruitPiece()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)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
}
}
},
});
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,
}
}
});
}
}
protected override void Update()
{
base.Update();
border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1);
}
private Drawable getFruitFor(FruitVisualRepresentation representation)
{
switch (representation)
{
case FruitVisualRepresentation.Pear:
return new PearPiece();
case FruitVisualRepresentation.Grape:
return new GrapePiece();
case FruitVisualRepresentation.Pineapple:
return new PineapplePiece();
case FruitVisualRepresentation.Banana:
return new BananaPiece();
case FruitVisualRepresentation.Raspberry:
return new RaspberryPiece();
}
return Empty();
}
}
}

View File

@ -2,10 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class BananaPiece : PulpFormation public class BananaPiece : PulpFormation
{ {

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
public class BorderPiece : Circle
{
public BorderPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
BorderColour = Color4.White;
BorderThickness = 6f * FruitPiece.RADIUS_ADJUST;
// Border is drawn only when there is a child drawable.
Child = new Box
{
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both,
};
}
}
}

View File

@ -0,0 +1,37 @@
// 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.Bindables;
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 readonly Bindable<bool> HyperDash = new Bindable<bool>();
public DropletPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
InternalChild = new Pulp
{
RelativeSizeAxes = Axes.Both,
AccentColour = { BindTarget = drawableObject.AccentColour }
};
if (HyperDash.Value)
{
AddInternal(new HyperDropletBorderPiece());
}
}
}
}

View File

@ -0,0 +1,79 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
internal class FruitPiece : CompositeDrawable
{
/// <summary>
/// Because we're adding a border around the fruit, we need to scale down some.
/// </summary>
public const float RADIUS_ADJUST = 1.1f;
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[CanBeNull]
private DrawableCatchHitObject drawableHitObject;
[CanBeNull]
private BorderPiece borderPiece;
public FruitPiece()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader(permitNulls: true)]
private void load([CanBeNull] DrawableHitObject drawable)
{
drawableHitObject = (DrawableCatchHitObject)drawable;
AddInternal(getFruitFor(VisualRepresentation.Value));
// if it is not part of a DHO, the border is always invisible.
if (drawableHitObject != null)
AddInternal(borderPiece = new BorderPiece());
if (HyperDash.Value)
AddInternal(new HyperBorderPiece());
}
protected override void Update()
{
if (borderPiece != null && drawableHitObject?.HitObject != null)
borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1);
}
private Drawable getFruitFor(FruitVisualRepresentation representation)
{
switch (representation)
{
case FruitVisualRepresentation.Pear:
return new PearPiece();
case FruitVisualRepresentation.Grape:
return new GrapePiece();
case FruitVisualRepresentation.Pineapple:
return new PineapplePiece();
case FruitVisualRepresentation.Banana:
return new BananaPiece();
case FruitVisualRepresentation.Raspberry:
return new RaspberryPiece();
}
return Empty();
}
}
}

View File

@ -2,10 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class GrapePiece : PulpFormation public class GrapePiece : PulpFormation
{ {

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
public class HyperBorderPiece : BorderPiece
{
public HyperBorderPiece()
{
BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
BorderThickness = 12f * FruitPiece.RADIUS_ADJUST;
Child.Alpha = 0.3f;
Child.Blending = BlendingParameters.Additive;
Child.Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR;
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{
public class HyperDropletBorderPiece : HyperBorderPiece
{
public HyperDropletBorderPiece()
{
Size /= 2;
BorderThickness = 6f;
}
}
}

View File

@ -2,10 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class PearPiece : PulpFormation public class PearPiece : PulpFormation
{ {

View File

@ -2,10 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class PineapplePiece : PulpFormation public class PineapplePiece : PulpFormation
{ {

View File

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

View File

@ -2,10 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
{ {
public class RaspberryPiece : PulpFormation public class RaspberryPiece : PulpFormation
{ {

View File

@ -0,0 +1,48 @@
// 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.Bindables;
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; }
public readonly Bindable<bool> HyperDashBindable = new Bindable<bool>();
/// <summary>
/// Whether this fruit can initiate a hyperdash.
/// </summary>
public bool HyperDash => HyperDashBindable.Value;
private CatchHitObject hyperDashTarget;
/// <summary>
/// The target fruit if we are to initiate a hyperdash.
/// </summary>
public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;
set
{
hyperDashTarget = value;
HyperDashBindable.Value = value != null;
}
}
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count];
}
}

View File

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

View File

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

View File

@ -57,19 +57,22 @@ namespace osu.Game.Rulesets.Catch.UI
}; };
} }
public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); protected override void LoadComplete()
public override void Add(DrawableHitObject h)
{ {
h.OnNewResult += onNewResult; base.LoadComplete();
h.OnRevertResult += onRevertResult;
base.Add(h); // these subscriptions need to be done post constructor to ensure externally bound components have a chance to populate required fields (ScoreProcessor / ComboAtJudgement in this case).
NewResult += onNewResult;
var fruit = (DrawableCatchHitObject)h; RevertResult += onRevertResult;
fruit.CheckPosition = CheckIfWeCanCatch;
} }
protected override void OnNewDrawableHitObject(DrawableHitObject d)
{
((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
}
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);

View File

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

View File

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

View File

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

View File

@ -24,7 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (hitWindows.IsHitResultAllowed(result)) if (hitWindows.IsHitResultAllowed(result))
{ {
AddStep("Show " + result.GetDescription(), () => SetContents(() => AddStep("Show " + result.GetDescription(), () => SetContents(() =>
new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
}, null)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModEasy : ModEasy public class ManiaModEasy : ModEasyWithExtraLives
{ {
public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!";
} }

View File

@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -19,22 +20,42 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
} }
protected override double FadeInDuration => 50; protected override void ApplyMissAnimations()
{
if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece))
{
// this is temporary logic until mania's skin transformer returns IAnimatableJudgements
JudgementBody.ScaleTo(1.6f);
JudgementBody.ScaleTo(1, 100, Easing.In);
JudgementBody.MoveTo(Vector2.Zero);
JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
JudgementBody.RotateTo(0);
JudgementBody.RotateTo(40, 800, Easing.InQuint);
JudgementBody.FadeOutFromOne(800);
LifetimeEnd = JudgementBody.LatestTransformEndTime;
}
base.ApplyMissAnimations();
}
protected override void ApplyHitAnimations() protected override void ApplyHitAnimations()
{ {
JudgementBody.ScaleTo(0.8f); JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic); JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250); JudgementBody.Delay(50)
this.Delay(FadeInDuration).FadeOut(200); .ScaleTo(0.75f, 250)
.FadeOut(200);
} }
protected override Drawable CreateDefaultJudgement(HitResult result) => new ManiaJudgementPiece(result); protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private class ManiaJudgementPiece : DefaultJudgementPiece private class DefaultManiaJudgementPiece : DefaultJudgementPiece
{ {
public ManiaJudgementPiece(HitResult result) public DefaultManiaJudgementPiece(HitResult result)
: base(result) : base(result)
{ {
} }

View File

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

View File

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

View File

@ -43,10 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great); showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any()); AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
AddAssert("judgement body immediately visible", AddAssert("hit lighting has no transforms", () => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => !judgement.Lighting.Transforms.Any()));
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha == 1)); AddAssert("hit lighting hidden", () => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha == 0));
AddAssert("hit lighting hidden",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha == 0));
} }
[Test] [Test]
@ -57,10 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great); showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any()); AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
AddAssert("judgement body not immediately visible", AddUntilStep("hit lighting shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any(judgement => judgement.Lighting.Alpha > 0));
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
AddAssert("hit lighting shown",
() => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha > 0));
} }
private void showResult(HitResult result) private void showResult(HitResult result)
@ -89,7 +84,13 @@ namespace osu.Game.Rulesets.Osu.Tests
Children = new Drawable[] Children = new Drawable[]
{ {
pool, pool,
pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j => pool.Get(j => j.Apply(new JudgementResult(new HitObject
{
StartTime = Time.Current
}, new Judgement())
{
Type = result,
}, null)).With(j =>
{ {
j.Anchor = Anchor.Centre; j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre; j.Origin = Anchor.Centre;
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestDrawableOsuJudgement : DrawableOsuJudgement private class TestDrawableOsuJudgement : DrawableOsuJudgement
{ {
public new SkinnableSprite Lighting => base.Lighting; public new SkinnableSprite Lighting => base.Lighting;
public new Container JudgementBody => base.JudgementBody; public new SkinnableDrawable JudgementBody => base.JudgementBody;
} }
} }
} }

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -94,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
addMultipleObjectsStep(); addMultipleObjectsStep();
AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); AddStep("move hitobject", () =>
{
var manualClock = new ManualClock();
followPointRenderer.Clock = new FramedClock(manualClock);
manualClock.CurrentTime = getObject(1).HitObject.StartTime;
followPointRenderer.UpdateSubTree();
getObject(2).HitObject.Position = new Vector2(300, 100);
});
assertGroups(); assertGroups();
assertDirections();
} }
[TestCase(0, 0)] // Start -> Start [TestCase(0, 0)] // Start -> Start
@ -207,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertGroups() private void assertGroups()
{ {
AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); AddAssert("has correct group count", () => followPointRenderer.Entries.Count == hitObjectContainer.Count);
AddAssert("group endpoints are correct", () => AddAssert("group endpoints are correct", () =>
{ {
for (int i = 0; i < hitObjectContainer.Count; i++) for (int i = 0; i < hitObjectContainer.Count; i++)
@ -215,10 +226,10 @@ namespace osu.Game.Rulesets.Osu.Tests
DrawableOsuHitObject expectedStart = getObject(i); DrawableOsuHitObject expectedStart = getObject(i);
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
if (getGroup(i).Start != expectedStart.HitObject) if (getEntry(i).Start != expectedStart.HitObject)
throw new AssertionException($"Object {i} expected to be the start of group {i}."); throw new AssertionException($"Object {i} expected to be the start of group {i}.");
if (getGroup(i).End != expectedEnd?.HitObject) if (getEntry(i).End != expectedEnd?.HitObject)
throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
} }
@ -238,6 +249,12 @@ namespace osu.Game.Rulesets.Osu.Tests
if (expectedEnd == null) if (expectedEnd == null)
continue; continue;
var manualClock = new ManualClock();
followPointRenderer.Clock = new FramedClock(manualClock);
manualClock.CurrentTime = expectedStart.HitObject.StartTime;
followPointRenderer.UpdateSubTree();
var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray(); var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray();
if (points.Length == 0) if (points.Length == 0)
continue; continue;
@ -255,7 +272,9 @@ namespace osu.Game.Rulesets.Osu.Tests
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; private FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType<FollowPointConnection>().Single(c => c.Entry == getEntry(index));
private class TestHitObjectContainer : Container<DrawableOsuHitObject> private class TestHitObjectContainer : Container<DrawableOsuHitObject>
{ {

View File

@ -1,17 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using osu.Game.Rulesets.Mods;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -38,13 +38,37 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
var playfield = new TestOsuPlayfield();
playfield.Add(drawable);
return playfield;
}
private Drawable testStream(float circleSize, bool auto = false)
{
var playfield = new TestOsuPlayfield();
Vector2 pos = new Vector2(-250, 0);
for (int i = 0; i <= 1000; i += 100)
{
playfield.Add(createSingle(circleSize, auto, i, pos));
pos.X += 50;
}
return playfield;
}
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset)
{ {
positionOffset ??= Vector2.Zero; positionOffset ??= Vector2.Zero;
var circle = new HitCircle var circle = new HitCircle
{ {
StartTime = Time.Current + 1000 + timeOffset, StartTime = Time.Current + 1000 + timeOffset,
Position = positionOffset.Value, Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
}; };
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
@ -53,31 +77,14 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable }); mod.ApplyToDrawableHitObjects(new[] { drawable });
return drawable; return drawable;
} }
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto) protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
{ {
Anchor = Anchor.Centre,
Depth = depthIndex++ Depth = depthIndex++
}; };
private Drawable testStream(float circleSize, bool auto = false)
{
var container = new Container { RelativeSizeAxes = Axes.Both };
Vector2 pos = new Vector2(-250, 0);
for (int i = 0; i <= 1000; i += 100)
{
container.Add(testSingle(circleSize, auto, i, pos));
pos.X += 50;
}
return container;
}
protected class TestDrawableHitCircle : DrawableHitCircle protected class TestDrawableHitCircle : DrawableHitCircle
{ {
private readonly bool auto; private readonly bool auto;
@ -101,5 +108,13 @@ namespace osu.Game.Rulesets.Osu.Tests
base.CheckForResult(userTriggered, timeOffset); base.CheckForResult(userTriggered, timeOffset);
} }
} }
protected class TestOsuPlayfield : OsuPlayfield
{
public TestOsuPlayfield()
{
RelativeSizeAxes = Axes.Both;
}
}
} }
} }

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -10,6 +13,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneShaking : TestSceneHitCircle public class TestSceneShaking : TestSceneHitCircle
{ {
private readonly List<ScheduledDelegate> scheduledTasks = new List<ScheduledDelegate>();
protected override IBeatmap CreateBeatmapForSkinProvider()
{
// best way to run cleanup before a new step is run
foreach (var task in scheduledTasks)
task.Cancel();
scheduledTasks.Clear();
return base.CreateBeatmapForSkinProvider();
}
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
{ {
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto); var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
@ -17,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Debug.Assert(drawableHitObject.HitObject.HitWindows != null); Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current; double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current;
Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay); scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay));
return drawableHitObject; return drawableHitObject;
} }

View File

@ -20,12 +20,15 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
{ {
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
internal readonly Container<PathControlPointPiece> Pieces; internal readonly Container<PathControlPointPiece> Pieces;
internal readonly Container<PathControlPointConnectionPiece> Connections; internal readonly Container<PathControlPointConnectionPiece> Connections;

View File

@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
public SliderSelectionBlueprint(DrawableSlider slider) public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider) : base(slider)
{ {
@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}; };
} }
private IBindable<int> pathVersion;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
pathVersion = HitObject.Path.Version.GetBoundCopy(); controlPoints.BindTo(HitObject.Path.ControlPoints);
pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => updatePath()); pathVersion.BindValueChanged(_ => updatePath());
BodyPiece.UpdateFrom(HitObject); BodyPiece.UpdateFrom(HitObject);
@ -164,8 +167,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
} }
private BindableList<PathControlPoint> controlPoints => HitObject.Path.ControlPoints;
private int addControlPoint(Vector2 position) private int addControlPoint(Vector2 position)
{ {
position -= HitObject.Position; position -= HitObject.Position;

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModEasy : ModEasy public class OsuModEasy : ModEasyWithExtraLives
{ {
public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
} }

View File

@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -15,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s. /// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPoint : Container, IAnimationTimeReference public class FollowPoint : PoolableDrawable, IAnimationTimeReference
{ {
private const float width = 8; private const float width = 8;
@ -25,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
{ {
Masking = true, Masking = true,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -46,6 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
}); });
} }
public double AnimationStartTime { get; set; } public Bindable<double> AnimationStartTime { get; } = new BindableDouble();
} }
} }

View File

@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
@ -15,150 +12,106 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s. /// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointConnection : CompositeDrawable public class FollowPointConnection : PoolableDrawable
{ {
// Todo: These shouldn't be constants // Todo: These shouldn't be constants
private const int spacing = 32; public const int SPACING = 32;
private const double preempt = 800; public const double PREEMPT = 800;
public override bool RemoveWhenNotAlive => false; public FollowPointLifetimeEntry Entry;
public DrawablePool<FollowPoint> Pool;
/// <summary> protected override void PrepareForUse()
/// The start time of <see cref="Start"/>.
/// </summary>
public readonly Bindable<double> StartTime = new BindableDouble();
/// <summary>
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.
/// </summary>
[NotNull]
public readonly OsuHitObject Start;
/// <summary>
/// Creates a new <see cref="FollowPointConnection"/>.
/// </summary>
/// <param name="start">The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.</param>
public FollowPointConnection([NotNull] OsuHitObject start)
{ {
Start = start; base.PrepareForUse();
RelativeSizeAxes = Axes.Both; Entry.Invalidated += onEntryInvalidated;
StartTime.BindTo(start.StartTimeBindable); refreshPoints();
} }
protected override void LoadComplete() protected override void FreeAfterUse()
{ {
base.LoadComplete(); base.FreeAfterUse();
bindEvents(Start);
Entry.Invalidated -= onEntryInvalidated;
// Return points to the pool.
ClearInternal(false);
Entry = null;
} }
private OsuHitObject end; private void onEntryInvalidated() => refreshPoints();
/// <summary> private void refreshPoints()
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will enter.
/// </summary>
[CanBeNull]
public OsuHitObject End
{ {
get => end; ClearInternal(false);
set
{
end = value;
if (end != null) OsuHitObject start = Entry.Start;
bindEvents(end); OsuHitObject end = Entry.End;
if (IsLoaded) double startTime = start.GetEndTime();
scheduleRefresh();
else
refresh();
}
}
private void bindEvents(OsuHitObject obj) Vector2 startPosition = start.StackedEndPosition;
{ Vector2 endPosition = end.StackedPosition;
obj.PositionBindable.BindValueChanged(_ => scheduleRefresh());
obj.DefaultsApplied += _ => scheduleRefresh();
}
private void scheduleRefresh()
{
Scheduler.AddOnce(refresh);
}
private void refresh()
{
double startTime = Start.GetEndTime();
LifetimeStart = startTime;
if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
{
// ensure we always set a lifetime for full LifetimeManagementContainer benefits
LifetimeEnd = LifetimeStart;
return;
}
Vector2 startPosition = Start.StackedEndPosition;
Vector2 endPosition = End.StackedPosition;
double endTime = End.StartTime;
Vector2 distanceVector = endPosition - startPosition; Vector2 distanceVector = endPosition - startPosition;
int distance = (int)distanceVector.Length; int distance = (int)distanceVector.Length;
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
double duration = endTime - startTime;
double? firstTransformStartTime = null;
double finalTransformEndTime = startTime; double finalTransformEndTime = startTime;
int point = 0; for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING)
ClearInternal();
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
{ {
float fraction = (float)d / distance; float fraction = (float)d / distance;
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector; Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
Vector2 pointEndPosition = startPosition + fraction * distanceVector; Vector2 pointEndPosition = startPosition + fraction * distanceVector;
double fadeOutTime = startTime + fraction * duration;
double fadeInTime = fadeOutTime - preempt; GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime);
FollowPoint fp; FollowPoint fp;
AddInternal(fp = new FollowPoint()); AddInternal(fp = Pool.Get());
Debug.Assert(End != null);
fp.ClearTransforms();
fp.Position = pointStartPosition; fp.Position = pointStartPosition;
fp.Rotation = rotation; fp.Rotation = rotation;
fp.Alpha = 0; fp.Alpha = 0;
fp.Scale = new Vector2(1.5f * End.Scale); fp.Scale = new Vector2(1.5f * end.Scale);
firstTransformStartTime ??= fadeInTime; fp.AnimationStartTime.Value = fadeInTime;
fp.AnimationStartTime = fadeInTime;
using (fp.BeginAbsoluteSequence(fadeInTime)) using (fp.BeginAbsoluteSequence(fadeInTime))
{ {
fp.FadeIn(End.TimeFadeIn); fp.FadeIn(end.TimeFadeIn);
fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out); fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out); fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn); fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn);
finalTransformEndTime = fadeOutTime + End.TimeFadeIn; finalTransformEndTime = fadeOutTime + end.TimeFadeIn;
} }
point++;
} }
int excessPoints = InternalChildren.Count - point;
for (int i = 0; i < excessPoints; i++)
RemoveInternal(InternalChildren[^1]);
// todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed.
LifetimeStart = firstTransformStartTime ?? startTime; Entry.LifetimeEnd = finalTransformEndTime;
LifetimeEnd = finalTransformEndTime; }
/// <summary>
/// Computes the fade time of follow point positioned between two hitobjects.
/// </summary>
/// <param name="start">The first <see cref="OsuHitObject"/>, where follow points should originate from.</param>
/// <param name="end">The second <see cref="OsuHitObject"/>, which follow points should target.</param>
/// <param name="fraction">The fractional distance along <paramref name="start"/> and <paramref name="end"/> at which the follow point is to be located.</param>
/// <param name="fadeInTime">The fade-in time of the follow point/</param>
/// <param name="fadeOutTime">The fade-out time of the follow point.</param>
public static void GetFadeTimes(OsuHitObject start, OsuHitObject end, float fraction, out double fadeInTime, out double fadeOutTime)
{
double startTime = start.GetEndTime();
double duration = end.StartTime - startTime;
fadeOutTime = startTime + fraction * duration;
fadeInTime = fadeOutTime - PREEMPT;
} }
} }
} }

View File

@ -0,0 +1,98 @@
// 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.Bindables;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
public class FollowPointLifetimeEntry : LifetimeEntry
{
public event Action Invalidated;
public readonly OsuHitObject Start;
public FollowPointLifetimeEntry(OsuHitObject start)
{
Start = start;
LifetimeStart = Start.StartTime;
bindEvents();
}
private OsuHitObject end;
public OsuHitObject End
{
get => end;
set
{
UnbindEvents();
end = value;
bindEvents();
refreshLifetimes();
}
}
private void bindEvents()
{
UnbindEvents();
// Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
Start.DefaultsApplied += onDefaultsApplied;
Start.PositionBindable.ValueChanged += onPositionChanged;
if (End != null)
{
End.DefaultsApplied += onDefaultsApplied;
End.PositionBindable.ValueChanged += onPositionChanged;
}
}
public void UnbindEvents()
{
if (Start != null)
{
Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged;
}
if (End != null)
{
End.DefaultsApplied -= onDefaultsApplied;
End.PositionBindable.ValueChanged -= onPositionChanged;
}
}
private void onDefaultsApplied(HitObject obj) => refreshLifetimes();
private void onPositionChanged(ValueChangedEvent<Vector2> obj) => refreshLifetimes();
private void refreshLifetimes()
{
if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
{
LifetimeEnd = LifetimeStart;
return;
}
Vector2 startPosition = Start.StackedEndPosition;
Vector2 endPosition = End.StackedPosition;
Vector2 distanceVector = endPosition - startPosition;
// The lifetime start will match the fade-in time of the first follow point.
float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length;
FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _);
LifetimeStart = fadeInTime;
LifetimeEnd = double.MaxValue; // This will be set by the connection.
Invalidated?.Invoke();
}
}
}

View File

@ -2,53 +2,74 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
/// <summary> /// <summary>
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s. /// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointRenderer : LifetimeManagementContainer public class FollowPointRenderer : CompositeDrawable
{ {
/// <summary>
/// All the <see cref="FollowPointConnection"/>s contained by this <see cref="FollowPointRenderer"/>.
/// </summary>
internal IReadOnlyList<FollowPointConnection> Connections => connections;
private readonly List<FollowPointConnection> connections = new List<FollowPointConnection>();
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
/// <summary> public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
/// Adds the <see cref="FollowPoint"/>s around an <see cref="OsuHitObject"/>.
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="OsuHitObject"/> to add <see cref="FollowPoint"/>s for.</param>
public void AddFollowPoints(OsuHitObject hitObject)
=> addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
/// <summary> private DrawablePool<FollowPointConnection> connectionPool;
/// Removes the <see cref="FollowPoint"/>s around an <see cref="OsuHitObject"/>. private DrawablePool<FollowPoint> pointPool;
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="OsuHitObject"/> to remove <see cref="FollowPoint"/>s for.</param>
public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
/// <summary> private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>. private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
/// </summary> private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param> private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
private void addConnection(FollowPointConnection connection) public FollowPointRenderer()
{ {
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => lifetimeManager.EntryBecameDead += onEntryBecameDead;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{ {
int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value); connectionPool = new DrawablePoolNoLifetime<FollowPointConnection>(1, 200),
pointPool = new DrawablePoolNoLifetime<FollowPoint>(50, 1000)
};
}
public void AddFollowPoints(OsuHitObject hitObject)
{
addEntry(hitObject);
var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject);
startTimeMap[hitObject] = startTimeBindable;
}
public void RemoveFollowPoints(OsuHitObject hitObject)
{
removeEntry(hitObject);
startTimeMap[hitObject].UnbindAll();
startTimeMap.Remove(hitObject);
}
private void addEntry(OsuHitObject hitObject)
{
var newEntry = new FollowPointLifetimeEntry(hitObject);
var index = lifetimeEntries.AddInPlace(newEntry, Comparer<FollowPointLifetimeEntry>.Create((e1, e2) =>
{
int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime);
if (comp != 0) if (comp != 0)
return comp; return comp;
@ -61,19 +82,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
return -1; return -1;
})); }));
if (index < connections.Count - 1) if (index < lifetimeEntries.Count - 1)
{ {
// Update the connection's end point to the next connection's start point // Update the connection's end point to the next connection's start point
// h1 -> -> -> h2 // h1 -> -> -> h2
// connection nextGroup // connection nextGroup
FollowPointConnection nextConnection = connections[index + 1]; FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1];
connection.End = nextConnection.Start; newEntry.End = nextEntry.Start;
} }
else else
{ {
// The end point may be non-null during re-ordering // The end point may be non-null during re-ordering
connection.End = null; newEntry.End = null;
} }
if (index > 0) if (index > 0)
@ -82,23 +103,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2 // h1 -> -> -> h2
// prevGroup connection // prevGroup connection
FollowPointConnection previousConnection = connections[index - 1]; FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
previousConnection.End = connection.Start; previousEntry.End = newEntry.Start;
} }
AddInternal(connection); lifetimeManager.AddEntry(newEntry);
} }
/// <summary> private void removeEntry(OsuHitObject hitObject)
/// Removes a <see cref="FollowPointConnection"/> from this <see cref="FollowPointRenderer"/>.
/// </summary>
/// <param name="connection">The <see cref="FollowPointConnection"/> to remove.</param>
/// <returns>Whether <paramref name="connection"/> was removed.</returns>
private void removeGroup(FollowPointConnection connection)
{ {
RemoveInternal(connection); int index = lifetimeEntries.FindIndex(e => e.Start == hitObject);
int index = connections.IndexOf(connection); var entry = lifetimeEntries[index];
entry.UnbindEvents();
lifetimeEntries.RemoveAt(index);
lifetimeManager.RemoveEntry(entry);
if (index > 0) if (index > 0)
{ {
@ -106,18 +126,61 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2 -> -> -> h3 // h1 -> -> -> h2 -> -> -> h3
// prevGroup connection nextGroup // prevGroup connection nextGroup
// The current connection's end point is used since there may not be a next connection // The current connection's end point is used since there may not be a next connection
FollowPointConnection previousConnection = connections[index - 1]; FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
previousConnection.End = connection.End; previousEntry.End = entry.End;
} }
connections.Remove(connection);
} }
private void onStartTimeChanged(FollowPointConnection connection) protected override bool CheckChildrenLife()
{ {
// Naive but can be improved if performance becomes an issue bool anyAliveChanged = base.CheckChildrenLife();
removeGroup(connection); anyAliveChanged |= lifetimeManager.Update(Time.Current);
addConnection(connection); return anyAliveChanged;
}
private void onEntryBecameAlive(LifetimeEntry entry)
{
var connection = connectionPool.Get(c =>
{
c.Entry = (FollowPointLifetimeEntry)entry;
c.Pool = pointPool;
});
connectionsInUse[entry] = connection;
AddInternal(connection);
}
private void onEntryBecameDead(LifetimeEntry entry)
{
RemoveInternal(connectionsInUse[entry]);
connectionsInUse.Remove(entry);
}
private void onStartTimeChanged(OsuHitObject hitObject)
{
removeEntry(hitObject);
addEntry(hitObject);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var entry in lifetimeEntries)
entry.UnbindEvents();
lifetimeEntries.Clear();
}
private class DrawablePoolNoLifetime<T> : DrawablePool<T>
where T : PoolableDrawable, new()
{
public override bool RemoveWhenNotAlive => false;
public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
}
} }
} }
} }

View File

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

View File

@ -53,9 +53,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}); });
} }
protected override void OnApply(HitObject hitObject) protected override void OnApply()
{ {
base.OnApply(hitObject); base.OnApply();
IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable); IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable);
PositionBindable.BindTo(HitObject.PositionBindable); PositionBindable.BindTo(HitObject.PositionBindable);
@ -70,9 +70,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
} }
protected override void OnFree(HitObject hitObject) protected override void OnFree()
{ {
base.OnFree(hitObject); base.OnFree();
IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable);
PositionBindable.UnbindFrom(HitObject.PositionBindable); PositionBindable.UnbindFrom(HitObject.PositionBindable);

View File

@ -44,26 +44,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
} }
private double fadeOutDelay;
protected override double FadeOutDelay => fadeOutDelay;
protected override void ApplyHitAnimations() protected override void ApplyHitAnimations()
{ {
bool hitLightingEnabled = config.Get<bool>(OsuSetting.HitLighting); bool hitLightingEnabled = config.Get<bool>(OsuSetting.HitLighting);
if (hitLightingEnabled) Lighting.Alpha = 0;
{
JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
if (hitLightingEnabled && Lighting.Drawable != null)
{
// todo: this animation changes slightly based on new/old legacy skin versions.
Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
}
else
{
JudgementBody.Alpha = 1;
}
fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay; // extend the lifetime to cover lighting fade
LifetimeEnd = Lighting.LatestTransformEndTime;
}
base.ApplyHitAnimations(); base.ApplyHitAnimations();
} }

View File

@ -1,32 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuPool<T> : DrawablePool<T>
where T : DrawableHitObject, new()
{
private readonly Func<DrawableHitObject, double, bool> checkHittable;
private readonly Action<Drawable> onLoaded;
public DrawableOsuPool(Func<DrawableHitObject, double, bool> checkHittable, Action<Drawable> onLoaded, int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
this.checkHittable = checkHittable;
this.onLoaded = onLoaded;
}
protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o =>
{
var osuObject = (DrawableOsuHitObject)(object)o;
osuObject.CheckHittable = checkHittable;
osuObject.OnLoadComplete += onLoaded;
});
}
}

View File

@ -20,7 +20,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach public class DrawableSlider : DrawableOsuHitObject
{ {
public new Slider HitObject => (Slider)base.HitObject; public new Slider HitObject => (Slider)base.HitObject;
@ -87,18 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample); Tracking.BindValueChanged(updateSlidingSample);
} }
protected override void OnApply(HitObject hitObject) protected override void OnApply()
{ {
base.OnApply(hitObject); base.OnApply();
// Ensure that the version will change after the upcoming BindTo(). // Ensure that the version will change after the upcoming BindTo().
pathVersion.Value = int.MaxValue; pathVersion.Value = int.MaxValue;
PathVersion.BindTo(HitObject.Path.Version); PathVersion.BindTo(HitObject.Path.Version);
} }
protected override void OnFree(HitObject hitObject) protected override void OnFree()
{ {
base.OnFree(hitObject); base.OnFree();
PathVersion.UnbindFrom(HitObject.Path.Version); PathVersion.UnbindFrom(HitObject.Path.Version);

View File

@ -4,7 +4,6 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -36,9 +35,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindValueChanged(_ => updatePosition()); pathVersion.BindValueChanged(_ => updatePosition());
} }
protected override void OnFree(HitObject hitObject) protected override void OnFree()
{ {
base.OnFree(hitObject); base.OnFree();
pathVersion.UnbindFrom(drawableSlider.PathVersion); pathVersion.UnbindFrom(drawableSlider.PathVersion);
} }

View File

@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}; };
} }
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly IBindable<Color4> accentColour = new Bindable<Color4>(); private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
@ -50,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
var drawableOsuObject = (DrawableOsuHitObject)drawableObject; var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
} }
@ -59,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
base.LoadComplete(); base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => accentColour.BindValueChanged(colour =>
{ {
explode.Colour = colour.NewValue; explode.Colour = colour.NewValue;
@ -68,15 +65,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}, true); }, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
} }
private void updateState(ValueChangedEvent<ArmedState> state) private void updateState(DrawableHitObject drawableObject, ArmedState state)
{ {
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{ {
glow.FadeOut(400); glow.FadeOut(400);
switch (state.NewValue) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
const double flash_in = 40; const double flash_in = 40;

View File

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

View File

@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
private SkinnableSpriteText hitCircleText; private SkinnableSpriteText hitCircleText;
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>(); private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
@ -113,7 +112,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (overlayAboveNumber) if (overlayAboveNumber)
AddInternal(hitCircleOverlay.CreateProxy()); AddInternal(hitCircleOverlay.CreateProxy());
state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
@ -137,19 +135,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber) if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateState;
updateState(drawableObject, drawableObject.State.Value);
} }
private void updateState(ValueChangedEvent<ArmedState> state) private void updateState(DrawableHitObject drawableObject, ArmedState state)
{ {
const double legacy_fade_duration = 240; const double legacy_fade_duration = 240;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{ {
switch (state.NewValue) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
circleSprites.FadeOut(legacy_fade_duration, Easing.Out); circleSprites.FadeOut(legacy_fade_duration, Easing.Out);

View File

@ -3,9 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
@ -20,15 +20,12 @@ using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
public class OsuPlayfield : Playfield public class OsuPlayfield : Playfield
{ {
public readonly Func<DrawableHitObject, double, bool> CheckHittable;
private readonly PlayfieldBorder playfieldBorder; private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles; private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies; private readonly ProxyContainer spinnerProxies;
@ -40,86 +37,81 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
private readonly Bindable<bool> playfieldBorderStyle = new BindableBool();
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>(); private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
private readonly Container judgementAboveHitObjectLayer;
public OsuPlayfield() public OsuPlayfield()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
playfieldBorder = new PlayfieldBorder playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
{ spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
RelativeSizeAxes = Axes.Both, followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
Depth = 3 judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
}, HitObjectContainer,
spinnerProxies = new ProxyContainer judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
{ approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
RelativeSizeAxes = Axes.Both
},
followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
judgementLayer = new JudgementContainer<DrawableOsuJudgement>
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
},
// Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal
// Todo: Remove when hitobjects are properly pooled
new SkinProvidingContainer(null)
{
Child = HitObjectContainer,
},
approachCircles = new ProxyContainer
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
},
}; };
hitPolicy = new OrderedHitPolicy(HitObjectContainer); hitPolicy = new OrderedHitPolicy(HitObjectContainer);
CheckHittable = hitPolicy.IsHittable;
var hitWindows = new OsuHitWindows(); var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result)); poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
AddRangeInternal(poolDictionary.Values); AddRangeInternal(poolDictionary.Values);
NewResult += onNewResult; NewResult += onNewResult;
} }
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
}
private void onDrawableHitObjectLoaded(Drawable drawable)
{
// note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded.
switch (drawable)
{
case DrawableSpinner _:
spinnerProxies.Add(drawable.CreateProxy());
break;
case DrawableHitCircle hitCircle:
approachCircles.Add(hitCircle.ProxiedLayer.CreateProxy());
break;
}
}
private void onJudgmentLoaded(DrawableOsuJudgement judgement)
{
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
}
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config) private void load(OsuRulesetConfigManager config)
{ {
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
registerPool<HitCircle, DrawableHitCircle>(10, 100); RegisterPool<HitCircle, DrawableHitCircle>(10, 100);
registerPool<Slider, DrawableSlider>(10, 100); RegisterPool<Slider, DrawableSlider>(10, 100);
registerPool<SliderHeadCircle, DrawableSliderHead>(10, 100); RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
registerPool<SliderTailCircle, DrawableSliderTail>(10, 100); RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100);
registerPool<SliderTick, DrawableSliderTick>(10, 100); RegisterPool<SliderTick, DrawableSliderTick>(10, 100);
registerPool<SliderRepeat, DrawableSliderRepeat>(5, 50); RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
registerPool<Spinner, DrawableSpinner>(2, 20); RegisterPool<Spinner, DrawableSpinner>(2, 20);
registerPool<SpinnerTick, DrawableSpinnerTick>(10, 100); RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 100);
registerPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100); RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100);
} }
private void registerPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(CreatePool<TDrawable>(initialSize, maximumSize));
protected virtual DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null)
where TDrawable : DrawableHitObject, new()
=> new DrawableOsuPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize);
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
protected override void OnHitObjectAdded(HitObject hitObject) protected override void OnHitObjectAdded(HitObject hitObject)
@ -134,27 +126,6 @@ namespace osu.Game.Rulesets.Osu.UI
followPoints.RemoveFollowPoints((OsuHitObject)hitObject); followPoints.RemoveFollowPoints((OsuHitObject)hitObject);
} }
public void OnHitObjectLoaded(Drawable drawable)
{
switch (drawable)
{
case DrawableSliderHead _:
case DrawableSliderTail _:
case DrawableSliderTick _:
case DrawableSliderRepeat _:
case DrawableSpinnerTick _:
break;
case DrawableSpinner _:
spinnerProxies.Add(drawable.CreateProxy());
break;
case IDrawableHitObjectWithProxiedApproach approach:
approachCircles.Add(approach.ProxiedLayer.CreateProxy());
break;
}
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
@ -178,11 +149,13 @@ namespace osu.Game.Rulesets.Osu.UI
private class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement> private class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement>
{ {
private readonly HitResult result; private readonly HitResult result;
private readonly Action<DrawableOsuJudgement> onLoaded;
public DrawableJudgementPool(HitResult result) public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
: base(10) : base(10)
{ {
this.result = result; this.result = result;
this.onLoaded = onLoaded;
} }
protected override DrawableOsuJudgement CreateNewDrawable() protected override DrawableOsuJudgement CreateNewDrawable()
@ -192,6 +165,8 @@ namespace osu.Game.Rulesets.Osu.UI
// just a placeholder to initialise the correct drawable hierarchy for this pool. // just a placeholder to initialise the correct drawable hierarchy for this pool.
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null); judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
onLoaded?.Invoke(judgement);
return judgement; return judgement;
} }
} }

View File

@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModEasy : ModEasy public class TaikoModEasy : ModEasy
{ {
public override string Description => @"Beats move slower, less accuracy required, and three lives!"; public override string Description => @"Beats move slower, and less accuracy required!";
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,79 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.NonVisual.Skinning
{
[HeadlessTest]
public class LegacySkinAnimationTest : OsuTestScene
{
private const string animation_name = "animation";
private const int frame_count = 6;
[Cached(typeof(IAnimationTimeReference))]
private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference();
private TextureAnimation animation;
[Test]
public void TestAnimationTimeReferenceChange()
{
ISkin skin = new TestSkin();
AddStep("get animation", () => Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false)));
AddAssert("frame count correct", () => animation.FrameCount == frame_count);
assertPlaybackPosition(0);
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
assertPlaybackPosition(-1000);
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
assertPlaybackPosition(-500);
}
private void assertPlaybackPosition(double expectedPosition)
=> AddAssert($"playback position is {expectedPosition}", () => animation.PlaybackPosition == expectedPosition);
private class TestSkin : ISkin
{
private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray();
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
return lookup_names.Contains(componentName) ? Texture.WhitePixel : null;
}
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException();
}
private class TestAnimationTimeReference : IAnimationTimeReference
{
public ManualClock ManualClock { get; }
public IFrameBasedClock Clock { get; }
public Bindable<double> AnimationStartTime { get; } = new BindableDouble();
public TestAnimationTimeReference()
{
ManualClock = new ManualClock();
Clock = new FramedClock(ManualClock);
}
}
}
}

View File

@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Background
private class FadeAccessibleResults : ResultsScreen private class FadeAccessibleResults : ResultsScreen
{ {
public FadeAccessibleResults(ScoreInfo score) public FadeAccessibleResults(ScoreInfo score)
: base(score) : base(score, true)
{ {
} }

View File

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

View File

@ -0,0 +1,39 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public class TestSceneParticleExplosion : OsuTestScene
{
private ParticleExplosion explosion;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
AddStep("create initial", () =>
{
Child = explosion = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(400)
};
});
AddWaitStep("wait for playback", 5);
AddRepeatStep(@"restart animation", () =>
{
explosion.Restart();
}, 10);
}
}
}

View File

@ -261,9 +261,9 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
protected override void OnApply(HitObject hitObject) protected override void OnApply()
{ {
base.OnApply(hitObject); base.OnApply();
Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200));
} }

View File

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

View File

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

View File

@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay; public HotkeyRetryOverlay RetryOverlay;
public TestResultsScreen(ScoreInfo score) public TestResultsScreen(ScoreInfo score)
: base(score) : base(score, true)
{ {
} }
@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay; public HotkeyRetryOverlay RetryOverlay;
public UnrankedSoloResultsScreen(ScoreInfo score) public UnrankedSoloResultsScreen(ScoreInfo score)
: base(score) : base(score, true)
{ {
Score.Beatmap.OnlineBeatmapID = 0; Score.Beatmap.OnlineBeatmapID = 0;
Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending;

View File

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

View File

@ -11,12 +11,12 @@ using osu.Framework.Allocation;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestScenePaginatedContainerHeader : OsuTestScene public class TestSceneProfileSubsectionHeader : OsuTestScene
{ {
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private PaginatedContainerHeader header; private ProfileSubsectionHeader header;
[Test] [Test]
public void TestHiddenCounter() public void TestHiddenCounter()
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createHeader(string text, CounterVisibilityState state, int initialValue = 0) private void createHeader(string text, CounterVisibilityState state, int initialValue = 0)
{ {
Clear(); Clear();
Add(header = new PaginatedContainerHeader(text, state) Add(header = new ProfileSubsectionHeader(text, state)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

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

View File

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

View File

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

View File

@ -0,0 +1,144 @@
// 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.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Graphics
{
/// <summary>
/// An explosion of textured particles based on how osu-stable randomises the explosion pattern.
/// </summary>
public class ParticleExplosion : Sprite
{
private readonly int particleCount;
private readonly double duration;
private double startTime;
private readonly List<ParticlePart> parts = new List<ParticlePart>();
public ParticleExplosion(Texture texture, int particleCount, double duration)
{
Texture = texture;
this.particleCount = particleCount;
this.duration = duration;
Blending = BlendingParameters.Additive;
}
protected override void LoadComplete()
{
base.LoadComplete();
Restart();
}
/// <summary>
/// Restart the animation from the current point in time.
/// Supports transform time offset chaining.
/// </summary>
public void Restart()
{
startTime = TransformStartTime;
this.FadeOutFromOne(duration);
parts.Clear();
for (int i = 0; i < particleCount; i++)
parts.Add(new ParticlePart(duration));
}
protected override void Update()
{
base.Update();
Invalidate(Invalidation.DrawNode);
}
protected override DrawNode CreateDrawNode() => new ParticleExplosionDrawNode(this);
private class ParticleExplosionDrawNode : SpriteDrawNode
{
private readonly List<ParticlePart> parts = new List<ParticlePart>();
private ParticleExplosion source => (ParticleExplosion)Source;
private double startTime;
private double currentTime;
private Vector2 sourceSize;
public ParticleExplosionDrawNode(Sprite source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
parts.Clear();
parts.AddRange(source.parts);
sourceSize = source.Size;
startTime = source.startTime;
currentTime = source.Time.Current;
}
protected override void Blit(Action<TexturedVertex2D> vertexAction)
{
var time = currentTime - startTime;
foreach (var p in parts)
{
Vector2 pos = p.PositionAtTime(time);
float alpha = p.AlphaAtTime(time);
var rect = new RectangleF(
pos.X * sourceSize.X - Texture.DisplayWidth / 2,
pos.Y * sourceSize.Y - Texture.DisplayHeight / 2,
Texture.DisplayWidth,
Texture.DisplayHeight);
// convert to screen space.
var quad = new Quad(
Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix),
Vector2Extensions.Transform(rect.BottomLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix)
);
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
null, TextureCoords);
}
}
}
private readonly struct ParticlePart
{
private readonly double duration;
private readonly float direction;
private readonly float distance;
public ParticlePart(double availableDuration)
{
distance = RNG.NextSingle(0.5f);
duration = RNG.NextDouble(availableDuration / 3, availableDuration);
direction = RNG.NextSingle(0, MathF.PI * 2);
}
public float AlphaAtTime(double time) => 1 - progressAtTime(time);
public Vector2 PositionAtTime(double time)
{
var travelledDistance = distance * progressAtTime(time);
return new Vector2(0.5f) + travelledDistance * new Vector2(MathF.Sin(direction), MathF.Cos(direction));
}
private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1);
}
}
}

View File

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

View File

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

View File

@ -69,6 +69,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
}; };
@ -163,10 +164,10 @@ namespace osu.Game.Input.Bindings
[Description("Toggle now playing overlay")] [Description("Toggle now playing overlay")]
ToggleNowPlaying, ToggleNowPlaying,
[Description("Previous Selection")] [Description("Previous selection")]
SelectPrevious, SelectPrevious,
[Description("Next Selection")] [Description("Next selection")]
SelectNext, SelectNext,
[Description("Home")] [Description("Home")]
@ -175,26 +176,29 @@ namespace osu.Game.Input.Bindings
[Description("Toggle notifications")] [Description("Toggle notifications")]
ToggleNotifications, ToggleNotifications,
[Description("Pause")] [Description("Pause gameplay")]
PauseGameplay, PauseGameplay,
// Editor // Editor
[Description("Setup Mode")] [Description("Setup mode")]
EditorSetupMode, EditorSetupMode,
[Description("Compose Mode")] [Description("Compose mode")]
EditorComposeMode, EditorComposeMode,
[Description("Design Mode")] [Description("Design mode")]
EditorDesignMode, EditorDesignMode,
[Description("Timing Mode")] [Description("Timing mode")]
EditorTimingMode, EditorTimingMode,
[Description("Hold for HUD")] [Description("Hold for HUD")]
HoldForHUD, HoldForHUD,
[Description("Random Skin")] [Description("Random skin")]
RandomSkin, RandomSkin,
[Description("Pause / resume replay")]
TogglePauseReplay,
} }
} }

View File

@ -420,7 +420,7 @@ namespace osu.Game
break; break;
case ScorePresentType.Results: case ScorePresentType.Results:
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo)); screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break; break;
} }
}, validScreens: new[] { typeof(PlaySongSelect) }); }, validScreens: new[] { typeof(PlaySongSelect) });

View File

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

View File

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

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Sections.Beatmaps namespace osu.Game.Overlays.Profile.Sections.Beatmaps
{ {
public class PaginatedBeatmapContainer : PaginatedContainer<APIBeatmapSet> public class PaginatedBeatmapContainer : PaginatedProfileSubsection<APIBeatmapSet>
{ {
private const float panel_padding = 10f; private const float panel_padding = 10f;
private readonly BeatmapSetType type; private readonly BeatmapSetType type;

View File

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

View File

@ -13,7 +13,7 @@ using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Sections.Historical namespace osu.Game.Overlays.Profile.Sections.Historical
{ {
public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer<APIUserMostPlayedBeatmap> public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection<APIUserMostPlayedBeatmap>
{ {
public PaginatedMostPlayedBeatmapContainer(Bindable<User> user) public PaginatedMostPlayedBeatmapContainer(Bindable<User> user)
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible) : base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Users;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class PlayHistorySubsection : ChartProfileSubsection
{
public PlayHistorySubsection(Bindable<User> user)
: base(user, "Play History")
{
}
protected override UserHistoryCount[] GetValues(User user) => user?.MonthlyPlaycounts;
}
}

View File

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

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Users;
using static osu.Game.Users.User;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class ReplaysSubsection : ChartProfileSubsection
{
public ReplaysSubsection(Bindable<User> user)
: base(user, "Replays Watched History")
{
}
protected override UserHistoryCount[] GetValues(User user) => user?.ReplaysWatchedCounts;
}
}

View File

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

View File

@ -11,7 +11,7 @@ using System.Collections.Generic;
namespace osu.Game.Overlays.Profile.Sections.Kudosu namespace osu.Game.Overlays.Profile.Sections.Kudosu
{ {
public class PaginatedKudosuHistoryContainer : PaginatedContainer<APIKudosuHistory> public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection<APIKudosuHistory>
{ {
public PaginatedKudosuHistoryContainer(Bindable<User> user) public PaginatedKudosuHistoryContainer(Bindable<User> user)
: base(user, missingText: "This user hasn't received any kudosu!") : base(user, missingText: "This user hasn't received any kudosu!")

View File

@ -6,62 +6,51 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Users; using osu.Game.Users;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections namespace osu.Game.Overlays.Profile.Sections
{ {
public abstract class PaginatedContainer<TModel> : FillFlowContainer public abstract class PaginatedProfileSubsection<TModel> : ProfileSubsection
{ {
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved]
protected RulesetStore Rulesets { get; private set; }
protected int VisiblePages; protected int VisiblePages;
protected int ItemsPerPage; protected int ItemsPerPage;
protected readonly Bindable<User> User = new Bindable<User>(); protected FillFlowContainer ItemsContainer { get; private set; }
protected FillFlowContainer ItemsContainer;
protected RulesetStore Rulesets;
private APIRequest<List<TModel>> retrievalRequest; private APIRequest<List<TModel>> retrievalRequest;
private CancellationTokenSource loadCancellation; private CancellationTokenSource loadCancellation;
private readonly string missingText;
private ShowMoreButton moreButton; private ShowMoreButton moreButton;
private OsuSpriteText missing; private OsuSpriteText missing;
private PaginatedContainerHeader header; private readonly string missingText;
private readonly string headerText; protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
private readonly CounterVisibilityState counterVisibilityState; : base(user, headerText, counterVisibilityState)
protected PaginatedContainer(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
{ {
this.headerText = headerText;
this.missingText = missingText; this.missingText = missingText;
this.counterVisibilityState = counterVisibilityState;
User.BindTo(user);
} }
[BackgroundDependencyLoader] protected override Drawable CreateContent() => new FillFlowContainer
private void load(RulesetStore rulesets)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical; Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
header = new PaginatedContainerHeader(headerText, counterVisibilityState)
{
Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
},
ItemsContainer = new FillFlowContainer ItemsContainer = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -81,13 +70,14 @@ namespace osu.Game.Overlays.Profile.Sections
Font = OsuFont.GetFont(size: 15), Font = OsuFont.GetFont(size: 15),
Text = missingText, Text = missingText,
Alpha = 0, Alpha = 0,
}, }
}; }
};
Rulesets = rulesets; protected override void LoadComplete()
{
User.ValueChanged += onUserChanged; base.LoadComplete();
User.TriggerChange(); User.BindValueChanged(onUserChanged, true);
} }
private void onUserChanged(ValueChangedEvent<User> e) private void onUserChanged(ValueChangedEvent<User> e)
@ -124,7 +114,7 @@ namespace osu.Game.Overlays.Profile.Sections
moreButton.Hide(); moreButton.Hide();
moreButton.IsLoading = false; moreButton.IsLoading = false;
if (!string.IsNullOrEmpty(missing.Text)) if (!string.IsNullOrEmpty(missingText))
missing.Show(); missing.Show();
return; return;
@ -142,8 +132,6 @@ namespace osu.Game.Overlays.Profile.Sections
protected virtual int GetCount(User user) => 0; protected virtual int GetCount(User user) => 0;
protected void SetCount(int value) => header.Current.Value = value;
protected virtual void OnItemsReceived(List<TModel> items) protected virtual void OnItemsReceived(List<TModel> items)
{ {
} }
@ -154,8 +142,9 @@ namespace osu.Game.Overlays.Profile.Sections
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing);
retrievalRequest?.Cancel(); retrievalRequest?.Cancel();
loadCancellation?.Cancel();
base.Dispose(isDisposing);
} }
} }
} }

View File

@ -0,0 +1,51 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
using JetBrains.Annotations;
namespace osu.Game.Overlays.Profile.Sections
{
public abstract class ProfileSubsection : FillFlowContainer
{
protected readonly Bindable<User> User = new Bindable<User>();
private readonly string headerText;
private readonly CounterVisibilityState counterVisibilityState;
private ProfileSubsectionHeader header;
protected ProfileSubsection(Bindable<User> user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
{
this.headerText = headerText;
this.counterVisibilityState = counterVisibilityState;
User.BindTo(user);
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Children = new[]
{
header = new ProfileSubsectionHeader(headerText, counterVisibilityState)
{
Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
},
CreateContent()
};
}
[NotNull]
protected abstract Drawable CreateContent();
protected void SetCount(int value) => header.Current.Value = value;
}
}

Some files were not shown because too many files have changed in this diff Show More