mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 23:22:55 +08:00
Merge branch 'master' into change-taiko-ez
This commit is contained in:
commit
827a089810
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1113.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1120.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -139,7 +139,7 @@ namespace osu.Desktop
|
||||
|
||||
// SDL2 DesktopWindow
|
||||
case DesktopWindow desktopWindow:
|
||||
desktopWindow.CursorState.Value |= CursorState.Hidden;
|
||||
desktopWindow.CursorState |= CursorState.Hidden;
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
desktopWindow.Title = Name;
|
||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||
|
@ -18,71 +18,42 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
base.LoadComplete();
|
||||
|
||||
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
|
||||
AddStep($"show {rep}", () => SetContents(() => createDrawable(rep)));
|
||||
AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep)));
|
||||
|
||||
AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
|
||||
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
|
||||
|
||||
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
|
||||
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true)));
|
||||
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true)));
|
||||
|
||||
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
|
||||
}
|
||||
|
||||
private Drawable createDrawableTinyDroplet()
|
||||
private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) =>
|
||||
setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash);
|
||||
|
||||
private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash);
|
||||
|
||||
private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet()));
|
||||
|
||||
private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false)
|
||||
{
|
||||
var droplet = new TestCatchTinyDroplet
|
||||
{
|
||||
Scale = 1.5f,
|
||||
};
|
||||
var hitObject = d.HitObject;
|
||||
hitObject.StartTime = 1000000000000;
|
||||
hitObject.Scale = 1.5f;
|
||||
|
||||
return new DrawableTinyDroplet(droplet)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.None,
|
||||
Position = Vector2.Zero,
|
||||
Alpha = 1,
|
||||
LifetimeStart = double.NegativeInfinity,
|
||||
LifetimeEnd = double.PositiveInfinity,
|
||||
};
|
||||
}
|
||||
if (hyperdash)
|
||||
hitObject.HyperDashTarget = new Banana();
|
||||
|
||||
private Drawable createDrawableDroplet(bool hyperdash = false)
|
||||
{
|
||||
var droplet = new TestCatchDroplet
|
||||
d.Anchor = Anchor.Centre;
|
||||
d.RelativePositionAxes = Axes.None;
|
||||
d.Position = Vector2.Zero;
|
||||
d.HitObjectApplied += _ =>
|
||||
{
|
||||
Scale = 1.5f,
|
||||
HyperDashTarget = hyperdash ? new Banana() : null
|
||||
};
|
||||
|
||||
return new DrawableDroplet(droplet)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.None,
|
||||
Position = Vector2.Zero,
|
||||
Alpha = 1,
|
||||
LifetimeStart = double.NegativeInfinity,
|
||||
LifetimeEnd = double.PositiveInfinity,
|
||||
};
|
||||
}
|
||||
|
||||
private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
|
||||
{
|
||||
Fruit fruit = new TestCatchFruit(rep)
|
||||
{
|
||||
Scale = 1.5f,
|
||||
HyperDashTarget = hyperdash ? new Banana() : null
|
||||
};
|
||||
|
||||
return new DrawableFruit(fruit)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.None,
|
||||
Position = Vector2.Zero,
|
||||
Alpha = 1,
|
||||
LifetimeStart = double.NegativeInfinity,
|
||||
LifetimeEnd = double.PositiveInfinity,
|
||||
d.LifetimeStart = double.NegativeInfinity;
|
||||
d.LifetimeEnd = double.PositiveInfinity;
|
||||
};
|
||||
return d;
|
||||
}
|
||||
|
||||
public class TestCatchFruit : Fruit
|
||||
@ -90,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
public TestCatchFruit(FruitVisualRepresentation rep)
|
||||
{
|
||||
VisualRepresentation = rep;
|
||||
StartTime = 1000000000000;
|
||||
}
|
||||
|
||||
public override FruitVisualRepresentation VisualRepresentation { get; }
|
||||
}
|
||||
|
||||
public class TestCatchDroplet : Droplet
|
||||
{
|
||||
public TestCatchDroplet()
|
||||
{
|
||||
StartTime = 1000000000000;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestCatchTinyDroplet : TinyDroplet
|
||||
{
|
||||
public TestCatchTinyDroplet()
|
||||
{
|
||||
StartTime = 1000000000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
/// </summary>
|
||||
public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
SetContents(() => new FillFlowContainer
|
||||
{
|
||||
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
protected abstract DrawableManiaHitObject CreateHitObject();
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
if (hitWindows.IsHitResultAllowed(result))
|
||||
{
|
||||
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,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
@ -26,6 +27,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFadeOnMiss()
|
||||
{
|
||||
AddStep("miss tick", () =>
|
||||
{
|
||||
foreach (var holdNote in holdNotes)
|
||||
holdNote.ChildrenOfType<DrawableHoldNoteHead>().First().MissForcefully();
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerable<DrawableHoldNote> holdNotes => CreatedDrawables.SelectMany(d => d.ChildrenOfType<DrawableHoldNote>());
|
||||
|
||||
protected override DrawableManiaHitObject CreateHitObject()
|
||||
{
|
||||
var note = new HoldNote { Duration = 1000 };
|
||||
|
@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
public double? HoldStartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hold note has been released too early and shouldn't give full score for the release.
|
||||
/// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
|
||||
/// </summary>
|
||||
public bool HasBroken { get; private set; }
|
||||
public double? HoldBrokenTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hold note has been released potentially without having caused a break.
|
||||
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
}
|
||||
|
||||
if (Tail.Judged && !Tail.IsHit)
|
||||
HasBroken = true;
|
||||
HoldBrokenTime = Time.Current;
|
||||
}
|
||||
|
||||
public bool OnPressed(ManiaAction action)
|
||||
@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
// If the key has been released too early, the user should not receive full score for the release
|
||||
if (!Tail.IsHit)
|
||||
HasBroken = true;
|
||||
HoldBrokenTime = Time.Current;
|
||||
|
||||
releaseTime = Time.Current;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
ApplyResult(r =>
|
||||
{
|
||||
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
||||
if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken))
|
||||
if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
|
||||
result = HitResult.Meh;
|
||||
|
||||
r.Type = result;
|
||||
|
@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
public class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
|
||||
{
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
/// <summary>
|
||||
/// References the time at which the user started holding the hold note.
|
||||
/// </summary>
|
||||
|
@ -18,9 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
public class LegacyBodyPiece : LegacyManiaColumnElement
|
||||
{
|
||||
private DrawableHoldNote holdNote;
|
||||
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
private readonly IBindable<bool> isHitting = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Stores the start time of the fade animation that plays when any of the nested
|
||||
/// hitobjects of the hold note are missed.
|
||||
/// </summary>
|
||||
private readonly Bindable<double?> missFadeTime = new Bindable<double?>();
|
||||
|
||||
[CanBeNull]
|
||||
private Drawable bodySprite;
|
||||
|
||||
@ -38,6 +46,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
|
||||
{
|
||||
holdNote = (DrawableHoldNote)drawableObject;
|
||||
|
||||
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
|
||||
?? $"mania-note{FallbackColumnIndex}L";
|
||||
|
||||
@ -92,11 +102,26 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
InternalChild = bodySprite;
|
||||
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
|
||||
var holdNote = (DrawableHoldNote)drawableObject;
|
||||
isHitting.BindTo(holdNote.IsHitting);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
isHitting.BindValueChanged(onIsHittingChanged, true);
|
||||
missFadeTime.BindValueChanged(onMissFadeTimeChanged, true);
|
||||
|
||||
holdNote.ApplyCustomUpdateState += applyCustomUpdateState;
|
||||
applyCustomUpdateState(holdNote, holdNote.State.Value);
|
||||
}
|
||||
|
||||
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
// ensure that the hold note is also faded out when the head/tail/any tick is missed.
|
||||
if (state == ArmedState.Miss)
|
||||
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
|
||||
}
|
||||
|
||||
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
||||
@ -158,10 +183,38 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
}
|
||||
}
|
||||
|
||||
private void onMissFadeTimeChanged(ValueChangedEvent<double?> missFadeTimeChange)
|
||||
{
|
||||
if (missFadeTimeChange.NewValue == null)
|
||||
return;
|
||||
|
||||
// this update could come from any nested object of the hold note (or even from an input).
|
||||
// make sure the transforms are consistent across all affected parts.
|
||||
using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value))
|
||||
{
|
||||
// colour and duration matches stable
|
||||
// transforms not applied to entire hold note in order to not affect hit lighting
|
||||
const double fade_duration = 60;
|
||||
|
||||
holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration);
|
||||
holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration);
|
||||
bodySprite?.FadeColour(Colour4.DarkGray, fade_duration);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
missFadeTime.Value ??= holdNote.HoldBrokenTime;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (holdNote != null)
|
||||
holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
|
||||
|
||||
lightContainer?.Expire();
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
if (result.IsHit)
|
||||
hitPolicy.HandleHit(judgedObject);
|
||||
|
||||
if (!result.IsHit || !DisplayJudgements.Value)
|
||||
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
|
||||
|
@ -1,10 +1,11 @@
|
||||
// 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.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
@ -19,22 +20,52 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override void ApplyMissAnimations()
|
||||
{
|
||||
if (JudgementText != null)
|
||||
JudgementText.Font = JudgementText.Font.With(size: 25);
|
||||
}
|
||||
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);
|
||||
|
||||
protected override double FadeInDuration => 50;
|
||||
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()
|
||||
{
|
||||
JudgementBody.ScaleTo(0.8f);
|
||||
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
|
||||
|
||||
JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250);
|
||||
this.Delay(FadeInDuration).FadeOut(200);
|
||||
JudgementBody.Delay(50)
|
||||
.ScaleTo(0.75f, 250)
|
||||
.FadeOut(200);
|
||||
}
|
||||
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||
|
||||
private class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||
{
|
||||
public DefaultManiaJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
JudgementText.Font = JudgementText.Font.With(size: 25);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
// Tick judgements should not display text.
|
||||
if (judgedObject is DrawableHoldNoteTick)
|
||||
return;
|
||||
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPool.Get(j =>
|
||||
{
|
||||
|
@ -43,10 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
showResult(HitResult.Great);
|
||||
|
||||
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
|
||||
AddAssert("judgement body immediately visible",
|
||||
() => 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 has no transforms", () => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => !judgement.Lighting.Transforms.Any()));
|
||||
AddAssert("hit lighting hidden", () => this.ChildrenOfType<TestDrawableOsuJudgement>().All(judgement => judgement.Lighting.Alpha == 0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -57,10 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
showResult(HitResult.Great);
|
||||
|
||||
AddUntilStep("judgements shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any());
|
||||
AddAssert("judgement body not immediately visible",
|
||||
() => 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));
|
||||
AddUntilStep("hit lighting shown", () => this.ChildrenOfType<TestDrawableOsuJudgement>().Any(judgement => judgement.Lighting.Alpha > 0));
|
||||
}
|
||||
|
||||
private void showResult(HitResult result)
|
||||
@ -89,7 +84,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Children = new Drawable[]
|
||||
{
|
||||
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.Origin = Anchor.Centre;
|
||||
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private class TestDrawableOsuJudgement : DrawableOsuJudgement
|
||||
{
|
||||
public new SkinnableSprite Lighting => base.Lighting;
|
||||
public new Container JudgementBody => base.JudgementBody;
|
||||
public new SkinnableDrawable JudgementBody => base.JudgementBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -94,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
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();
|
||||
assertDirections();
|
||||
}
|
||||
|
||||
[TestCase(0, 0)] // Start -> Start
|
||||
@ -207,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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", () =>
|
||||
{
|
||||
for (int i = 0; i < hitObjectContainer.Count; i++)
|
||||
@ -215,10 +226,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
DrawableOsuHitObject expectedStart = getObject(i);
|
||||
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}.");
|
||||
|
||||
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}.");
|
||||
}
|
||||
|
||||
@ -238,6 +249,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
if (expectedEnd == null)
|
||||
continue;
|
||||
|
||||
var manualClock = new ManualClock();
|
||||
followPointRenderer.Clock = new FramedClock(manualClock);
|
||||
|
||||
manualClock.CurrentTime = expectedStart.HitObject.StartTime;
|
||||
followPointRenderer.UpdateSubTree();
|
||||
|
||||
var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray();
|
||||
if (points.Length == 0)
|
||||
continue;
|
||||
@ -255,7 +272,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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>
|
||||
{
|
||||
|
@ -1,17 +1,17 @@
|
||||
// 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.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 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 osuTK;
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
var circle = new HitCircle
|
||||
{
|
||||
StartTime = Time.Current + 1000 + timeOffset,
|
||||
Position = positionOffset.Value,
|
||||
Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
|
||||
};
|
||||
|
||||
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>())
|
||||
mod.ApplyToDrawableHitObjects(new[] { drawable });
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
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
|
||||
{
|
||||
private readonly bool auto;
|
||||
@ -101,5 +108,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
}
|
||||
}
|
||||
|
||||
protected class TestOsuPlayfield : OsuPlayfield
|
||||
{
|
||||
public TestOsuPlayfield()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -10,6 +13,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
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)
|
||||
{
|
||||
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
|
||||
@ -17,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private readonly Path path;
|
||||
private readonly Slider slider;
|
||||
private readonly int controlPointIndex;
|
||||
public int ControlPointIndex { get; set; }
|
||||
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<int> pathVersion;
|
||||
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.controlPointIndex = controlPointIndex;
|
||||
ControlPointIndex = controlPointIndex;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
path.ClearVertices();
|
||||
|
||||
int nextIndex = controlPointIndex + 1;
|
||||
int nextIndex = ControlPointIndex + 1;
|
||||
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
|
||||
return;
|
||||
|
||||
|
@ -20,12 +20,15 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
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<PathControlPointConnectionPiece> Connections;
|
||||
|
||||
@ -66,6 +69,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
// If inserting in the path (not appending),
|
||||
// update indices of existing connections after insert location
|
||||
if (e.NewStartingIndex < Pieces.Count)
|
||||
{
|
||||
foreach (var connection in Connections)
|
||||
{
|
||||
if (connection.ControlPointIndex >= e.NewStartingIndex)
|
||||
connection.ControlPointIndex += e.NewItems.Count;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
{
|
||||
var point = (PathControlPoint)e.NewItems[i];
|
||||
@ -88,6 +102,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
Connections.RemoveAll(c => c.ControlPoint == point);
|
||||
}
|
||||
|
||||
// If removing before the end of the path,
|
||||
// update indices of connections after remove location
|
||||
if (e.OldStartingIndex < Pieces.Count)
|
||||
{
|
||||
foreach (var connection in Connections)
|
||||
{
|
||||
if (connection.ControlPointIndex >= e.OldStartingIndex)
|
||||
connection.ControlPointIndex -= e.OldItems.Count;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved(CanBeNull = true)]
|
||||
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)
|
||||
: base(slider)
|
||||
{
|
||||
@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
};
|
||||
}
|
||||
|
||||
private IBindable<int> pathVersion;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
pathVersion = HitObject.Path.Version.GetBoundCopy();
|
||||
controlPoints.BindTo(HitObject.Path.ControlPoints);
|
||||
|
||||
pathVersion.BindTo(HitObject.Path.Version);
|
||||
pathVersion.BindValueChanged(_ => updatePath());
|
||||
|
||||
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)
|
||||
{
|
||||
position -= HitObject.Position;
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
/// <summary>
|
||||
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
|
||||
/// </summary>
|
||||
public class FollowPoint : Container, IAnimationTimeReference
|
||||
public class FollowPoint : PoolableDrawable, IAnimationTimeReference
|
||||
{
|
||||
private const float width = 8;
|
||||
|
||||
@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
|
||||
InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
|
||||
{
|
||||
Masking = true,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
|
@ -2,11 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
|
||||
@ -15,150 +12,106 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
/// <summary>
|
||||
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
|
||||
/// </summary>
|
||||
public class FollowPointConnection : CompositeDrawable
|
||||
public class FollowPointConnection : PoolableDrawable
|
||||
{
|
||||
// Todo: These shouldn't be constants
|
||||
private const int spacing = 32;
|
||||
private const double preempt = 800;
|
||||
public const int SPACING = 32;
|
||||
public const double PREEMPT = 800;
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
public FollowPointLifetimeEntry Entry;
|
||||
public DrawablePool<FollowPoint> Pool;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
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();
|
||||
bindEvents(Start);
|
||||
base.FreeAfterUse();
|
||||
|
||||
Entry.Invalidated -= onEntryInvalidated;
|
||||
|
||||
// Return points to the pool.
|
||||
ClearInternal(false);
|
||||
|
||||
Entry = null;
|
||||
}
|
||||
|
||||
private OsuHitObject end;
|
||||
private void onEntryInvalidated() => refreshPoints();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will enter.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public OsuHitObject End
|
||||
private void refreshPoints()
|
||||
{
|
||||
get => end;
|
||||
set
|
||||
{
|
||||
end = value;
|
||||
ClearInternal(false);
|
||||
|
||||
if (end != null)
|
||||
bindEvents(end);
|
||||
OsuHitObject start = Entry.Start;
|
||||
OsuHitObject end = Entry.End;
|
||||
|
||||
if (IsLoaded)
|
||||
scheduleRefresh();
|
||||
else
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
double startTime = start.GetEndTime();
|
||||
|
||||
private void bindEvents(OsuHitObject obj)
|
||||
{
|
||||
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 startPosition = start.StackedEndPosition;
|
||||
Vector2 endPosition = end.StackedPosition;
|
||||
|
||||
Vector2 distanceVector = endPosition - startPosition;
|
||||
int distance = (int)distanceVector.Length;
|
||||
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
|
||||
double duration = endTime - startTime;
|
||||
|
||||
double? firstTransformStartTime = null;
|
||||
double finalTransformEndTime = startTime;
|
||||
|
||||
int point = 0;
|
||||
|
||||
ClearInternal();
|
||||
|
||||
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
|
||||
for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING)
|
||||
{
|
||||
float fraction = (float)d / distance;
|
||||
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * 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;
|
||||
|
||||
AddInternal(fp = new FollowPoint());
|
||||
|
||||
Debug.Assert(End != null);
|
||||
AddInternal(fp = Pool.Get());
|
||||
|
||||
fp.ClearTransforms();
|
||||
fp.Position = pointStartPosition;
|
||||
fp.Rotation = rotation;
|
||||
fp.Alpha = 0;
|
||||
fp.Scale = new Vector2(1.5f * End.Scale);
|
||||
|
||||
firstTransformStartTime ??= fadeInTime;
|
||||
fp.Scale = new Vector2(1.5f * end.Scale);
|
||||
|
||||
fp.AnimationStartTime = fadeInTime;
|
||||
|
||||
using (fp.BeginAbsoluteSequence(fadeInTime))
|
||||
{
|
||||
fp.FadeIn(End.TimeFadeIn);
|
||||
fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out);
|
||||
fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out);
|
||||
fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn);
|
||||
fp.FadeIn(end.TimeFadeIn);
|
||||
fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
|
||||
fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
|
||||
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.
|
||||
LifetimeStart = firstTransformStartTime ?? startTime;
|
||||
LifetimeEnd = finalTransformEndTime;
|
||||
Entry.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,53 +2,74 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
|
||||
/// </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;
|
||||
|
||||
/// <summary>
|
||||
/// 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))));
|
||||
public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Removes 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 remove <see cref="FollowPoint"/>s for.</param>
|
||||
public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
|
||||
private DrawablePool<FollowPointConnection> connectionPool;
|
||||
private DrawablePool<FollowPoint> pointPool;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param>
|
||||
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
|
||||
private void addConnection(FollowPointConnection connection)
|
||||
private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
|
||||
private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
|
||||
private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
|
||||
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
||||
|
||||
public FollowPointRenderer()
|
||||
{
|
||||
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
|
||||
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) =>
|
||||
lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
|
||||
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)
|
||||
return comp;
|
||||
@ -61,19 +82,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
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
|
||||
// h1 -> -> -> h2
|
||||
// connection nextGroup
|
||||
|
||||
FollowPointConnection nextConnection = connections[index + 1];
|
||||
connection.End = nextConnection.Start;
|
||||
FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1];
|
||||
newEntry.End = nextEntry.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The end point may be non-null during re-ordering
|
||||
connection.End = null;
|
||||
newEntry.End = null;
|
||||
}
|
||||
|
||||
if (index > 0)
|
||||
@ -82,23 +103,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
// h1 -> -> -> h2
|
||||
// prevGroup connection
|
||||
|
||||
FollowPointConnection previousConnection = connections[index - 1];
|
||||
previousConnection.End = connection.Start;
|
||||
FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
|
||||
previousEntry.End = newEntry.Start;
|
||||
}
|
||||
|
||||
AddInternal(connection);
|
||||
lifetimeManager.AddEntry(newEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
private void removeEntry(OsuHitObject hitObject)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -106,18 +126,61 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
// h1 -> -> -> h2 -> -> -> h3
|
||||
// prevGroup connection nextGroup
|
||||
// The current connection's end point is used since there may not be a next connection
|
||||
FollowPointConnection previousConnection = connections[index - 1];
|
||||
previousConnection.End = connection.End;
|
||||
FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
|
||||
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
|
||||
removeGroup(connection);
|
||||
addConnection(connection);
|
||||
bool anyAliveChanged = base.CheckChildrenLife();
|
||||
anyAliveChanged |= lifetimeManager.Update(Time.Current);
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
this.Delay(800).FadeOut();
|
||||
break;
|
||||
}
|
||||
|
||||
Expire();
|
||||
}
|
||||
|
||||
public Drawable ProxiedLayer => ApproachCircle;
|
||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@ -60,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
PositionBindable.BindTo(HitObject.PositionBindable);
|
||||
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
|
||||
ScaleBindable.BindTo(HitObject.ScaleBindable);
|
||||
|
||||
// Manually set to reduce the number of future alive objects to a bare minimum.
|
||||
LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
|
||||
|
||||
// Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts.
|
||||
// An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry.
|
||||
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
|
||||
}
|
||||
|
||||
protected override void OnFree(HitObject hitObject)
|
||||
@ -85,14 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// Manually set to reduce the number of future alive objects to a bare minimum.
|
||||
LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
|
@ -4,9 +4,9 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@ -17,15 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
|
||||
: base(result, judgedObject)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableOsuJudgement()
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -39,48 +30,55 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
});
|
||||
}
|
||||
|
||||
public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
|
||||
{
|
||||
base.Apply(result, judgedObject);
|
||||
|
||||
if (judgedObject?.HitObject is OsuHitObject osuObject)
|
||||
{
|
||||
Position = osuObject.StackedPosition;
|
||||
Scale = new Vector2(osuObject.Scale);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Lighting.ResetAnimation();
|
||||
Lighting.SetColourFrom(JudgedObject, Result);
|
||||
}
|
||||
|
||||
private double fadeOutDelay;
|
||||
protected override double FadeOutDelay => fadeOutDelay;
|
||||
if (JudgedObject?.HitObject is OsuHitObject osuObject)
|
||||
{
|
||||
Position = osuObject.StackedPosition;
|
||||
Scale = new Vector2(osuObject.Scale);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyHitAnimations()
|
||||
{
|
||||
bool hitLightingEnabled = config.Get<bool>(OsuSetting.HitLighting);
|
||||
|
||||
if (hitLightingEnabled)
|
||||
{
|
||||
JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
|
||||
Lighting.Alpha = 0;
|
||||
|
||||
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.FadeIn(200).Then().Delay(200).FadeOut(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
JudgementBody.Alpha = 1;
|
||||
|
||||
// extend the lifetime to cover lighting fade
|
||||
LifetimeEnd = Lighting.LatestTransformEndTime;
|
||||
}
|
||||
|
||||
fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
|
||||
|
||||
JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
|
||||
base.ApplyHitAnimations();
|
||||
}
|
||||
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result);
|
||||
|
||||
private class OsuJudgementPiece : DefaultJudgementPiece
|
||||
{
|
||||
public OsuJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
}
|
||||
|
||||
public override void PlayAnimation()
|
||||
{
|
||||
base.PlayAnimation();
|
||||
|
||||
if (Result != HitResult.Miss)
|
||||
JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -193,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
Body.FadeInFromZero(HitObject.TimeFadeIn);
|
||||
}
|
||||
|
||||
public readonly Bindable<bool> Tracking = new Bindable<bool>();
|
||||
|
||||
protected override void Update()
|
||||
@ -273,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.PlaySamples();
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
Body.FadeInFromZero(HitObject.TimeFadeIn);
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms()
|
||||
{
|
||||
base.UpdateStartTimeStateTransforms();
|
||||
@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
break;
|
||||
}
|
||||
|
||||
this.FadeOut(fade_out_time, Easing.OutQuint);
|
||||
this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
|
||||
}
|
||||
|
||||
public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;
|
||||
|
@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
|
||||
this.FadeOut(160);
|
||||
this.FadeOut(160).Expire();
|
||||
|
||||
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
|
||||
isSpinning?.TriggerChange();
|
||||
|
@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class CirclePiece : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private DrawableHitObject drawableObject { get; set; }
|
||||
|
||||
private TrianglesPiece triangles;
|
||||
|
||||
public CirclePiece()
|
||||
{
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
@ -26,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures, DrawableHitObject drawableHitObject)
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -36,13 +41,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
Origin = Anchor.Centre,
|
||||
Texture = textures.Get(@"Gameplay/osu/disc"),
|
||||
},
|
||||
new TrianglesPiece(drawableHitObject.GetHashCode())
|
||||
triangles = new TrianglesPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0.5f,
|
||||
}
|
||||
};
|
||||
|
||||
drawableObject.HitObjectApplied += onHitObjectApplied;
|
||||
onHitObjectApplied(drawableObject);
|
||||
}
|
||||
|
||||
private void onHitObjectApplied(DrawableHitObject obj)
|
||||
{
|
||||
if (obj.HitObject == null)
|
||||
return;
|
||||
|
||||
triangles.Reset((int)obj.HitObject.StartTime);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableObject != null)
|
||||
drawableObject.HitObjectApplied -= onHitObjectApplied;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class ExplodePiece : Container
|
||||
{
|
||||
[Resolved]
|
||||
private DrawableHitObject drawableObject { get; set; }
|
||||
|
||||
private TrianglesPiece triangles;
|
||||
|
||||
public ExplodePiece()
|
||||
{
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
@ -18,13 +25,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
|
||||
Blending = BlendingParameters.Additive;
|
||||
Alpha = 0;
|
||||
}
|
||||
|
||||
Child = new TrianglesPiece
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = triangles = new TrianglesPiece
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.2f,
|
||||
};
|
||||
|
||||
drawableObject.HitObjectApplied += onHitObjectApplied;
|
||||
onHitObjectApplied(drawableObject);
|
||||
}
|
||||
|
||||
private void onHitObjectApplied(DrawableHitObject obj)
|
||||
{
|
||||
if (obj.HitObject == null)
|
||||
return;
|
||||
|
||||
triangles.Reset((int)obj.HitObject.StartTime);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableObject != null)
|
||||
drawableObject.HitObjectApplied -= onHitObjectApplied;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
{
|
||||
public class TrianglesPiece : Triangles
|
||||
{
|
||||
protected override bool ExpireOffScreenTriangles => false;
|
||||
protected override bool CreateNewTriangles => false;
|
||||
protected override float SpawnRatio => 0.5f;
|
||||
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
@ -20,7 +19,6 @@ using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
@ -40,44 +38,21 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
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 Container judgementAboveHitObjectLayer;
|
||||
|
||||
public OsuPlayfield()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
playfieldBorder = new PlayfieldBorder
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 3
|
||||
},
|
||||
spinnerProxies = new ProxyContainer
|
||||
{
|
||||
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,
|
||||
},
|
||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
||||
followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
|
||||
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
|
||||
HitObjectContainer,
|
||||
judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
|
||||
approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
||||
};
|
||||
|
||||
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
||||
@ -86,13 +61,18 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
var hitWindows = new OsuHitWindows();
|
||||
|
||||
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);
|
||||
|
||||
NewResult += onNewResult;
|
||||
}
|
||||
|
||||
private void onJudgmentLoaded(DrawableOsuJudgement judgement)
|
||||
{
|
||||
judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuRulesetConfigManager config)
|
||||
{
|
||||
@ -178,11 +158,13 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
private class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement>
|
||||
{
|
||||
private readonly HitResult result;
|
||||
private readonly Action<DrawableOsuJudgement> onLoaded;
|
||||
|
||||
public DrawableJudgementPool(HitResult result)
|
||||
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
|
||||
: base(10)
|
||||
{
|
||||
this.result = result;
|
||||
this.onLoaded = onLoaded;
|
||||
}
|
||||
|
||||
protected override DrawableOsuJudgement CreateNewDrawable()
|
||||
@ -192,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
// just a placeholder to initialise the correct drawable hierarchy for this pool.
|
||||
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
|
||||
|
||||
onLoaded?.Invoke(judgement);
|
||||
|
||||
return judgement;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,9 @@
|
||||
// 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.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
@ -25,21 +22,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
switch (Result.Type)
|
||||
{
|
||||
case HitResult.Ok:
|
||||
JudgementBody.Colour = colours.GreenLight;
|
||||
break;
|
||||
|
||||
case HitResult.Great:
|
||||
JudgementBody.Colour = colours.BlueLight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyHitAnimations()
|
||||
{
|
||||
this.MoveToY(-100, 500);
|
||||
|
@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
private class FadeAccessibleResults : ResultsScreen
|
||||
{
|
||||
public FadeAccessibleResults(ScoreInfo score)
|
||||
: base(score)
|
||||
: base(score, true)
|
||||
{
|
||||
}
|
||||
|
||||
|
39
osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
Normal file
39
osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
public HotkeyRetryOverlay RetryOverlay;
|
||||
|
||||
public TestResultsScreen(ScoreInfo score)
|
||||
: base(score)
|
||||
: base(score, true)
|
||||
{
|
||||
}
|
||||
|
||||
@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
public HotkeyRetryOverlay RetryOverlay;
|
||||
|
||||
public UnrankedSoloResultsScreen(ScoreInfo score)
|
||||
: base(score)
|
||||
: base(score, true)
|
||||
{
|
||||
Score.Beatmap.OnlineBeatmapID = 0;
|
||||
Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending;
|
||||
|
@ -11,12 +11,12 @@ using osu.Framework.Allocation;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public class TestScenePaginatedContainerHeader : OsuTestScene
|
||||
public class TestSceneProfileSubsectionHeader : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
||||
|
||||
private PaginatedContainerHeader header;
|
||||
private ProfileSubsectionHeader header;
|
||||
|
||||
[Test]
|
||||
public void TestHiddenCounter()
|
||||
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private void createHeader(string text, CounterVisibilityState state, int initialValue = 0)
|
||||
{
|
||||
Clear();
|
||||
Add(header = new PaginatedContainerHeader(text, state)
|
||||
Add(header = new ProfileSubsectionHeader(text, state)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
@ -1,7 +1,6 @@
|
||||
// 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 System.Linq;
|
||||
using System.Threading;
|
||||
@ -15,103 +14,93 @@ namespace osu.Game.Database
|
||||
{
|
||||
public class UserLookupCache : MemoryCachingComponent<int, User>
|
||||
{
|
||||
private readonly HashSet<int> nextTaskIDs = new HashSet<int>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
private readonly object taskAssignmentLock = new object();
|
||||
|
||||
private Task<List<User>> pendingRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="pendingRequest"/> has already grabbed its IDs.
|
||||
/// </summary>
|
||||
private bool pendingRequestConsumedIDs;
|
||||
|
||||
public Task<User> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
|
||||
|
||||
protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
|
||||
{
|
||||
var users = await getQueryTaskForUser(lookup);
|
||||
return users.FirstOrDefault(u => u.Id == lookup);
|
||||
}
|
||||
=> await queryUser(lookup);
|
||||
|
||||
/// <summary>
|
||||
/// Return the task responsible for fetching the provided user.
|
||||
/// This may be part of a larger batch lookup to reduce web requests.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to lookup.</param>
|
||||
/// <returns>The task responsible for the lookup.</returns>
|
||||
private Task<List<User>> getQueryTaskForUser(int userId)
|
||||
private readonly Queue<(int id, TaskCompletionSource<User>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<User>)>();
|
||||
private Task pendingRequestTask;
|
||||
private readonly object taskAssignmentLock = new object();
|
||||
|
||||
private Task<User> queryUser(int userId)
|
||||
{
|
||||
lock (taskAssignmentLock)
|
||||
{
|
||||
nextTaskIDs.Add(userId);
|
||||
var tcs = new TaskCompletionSource<User>();
|
||||
|
||||
// if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it.
|
||||
if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50)
|
||||
return pendingRequest;
|
||||
// Add to the queue.
|
||||
pendingUserTasks.Enqueue((userId, tcs));
|
||||
|
||||
return queueNextTask(nextLookup);
|
||||
// Create a request task if there's not already one.
|
||||
if (pendingRequestTask == null)
|
||||
createNewTask();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
List<User> nextLookup()
|
||||
private void performLookup()
|
||||
{
|
||||
// contains at most 50 unique user IDs from userTasks, which is used to perform the lookup.
|
||||
var userTasks = new Dictionary<int, List<TaskCompletionSource<User>>>();
|
||||
|
||||
// Grab at most 50 unique user IDs from the queue.
|
||||
lock (taskAssignmentLock)
|
||||
{
|
||||
int[] lookupItems;
|
||||
|
||||
lock (taskAssignmentLock)
|
||||
while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
|
||||
{
|
||||
pendingRequestConsumedIDs = true;
|
||||
lookupItems = nextTaskIDs.ToArray();
|
||||
nextTaskIDs.Clear();
|
||||
(int id, TaskCompletionSource<User> task) next = pendingUserTasks.Dequeue();
|
||||
|
||||
if (lookupItems.Length == 0)
|
||||
// Perform a secondary check for existence, in case the user was queried in a previous batch.
|
||||
if (CheckExists(next.id, out var existing))
|
||||
next.task.SetResult(existing);
|
||||
else
|
||||
{
|
||||
queueNextTask(null);
|
||||
return new List<User>();
|
||||
if (userTasks.TryGetValue(next.id, out var tasks))
|
||||
tasks.Add(next.task);
|
||||
else
|
||||
userTasks[next.id] = new List<TaskCompletionSource<User>> { next.task };
|
||||
}
|
||||
}
|
||||
|
||||
var request = new GetUsersRequest(lookupItems);
|
||||
|
||||
// rather than queueing, we maintain our own single-threaded request stream.
|
||||
api.Perform(request);
|
||||
|
||||
return request.Result?.Users;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues new work at the end of the current work tasks.
|
||||
/// Ensures the provided work is eventually run.
|
||||
/// </summary>
|
||||
/// <param name="work">The work to run. Can be null to signify the end of available work.</param>
|
||||
/// <returns>The task tracking this work.</returns>
|
||||
private Task<List<User>> queueNextTask(Func<List<User>> work)
|
||||
{
|
||||
// Query the users.
|
||||
var request = new GetUsersRequest(userTasks.Keys.ToArray());
|
||||
|
||||
// rather than queueing, we maintain our own single-threaded request stream.
|
||||
api.Perform(request);
|
||||
|
||||
// Create a new request task if there's still more users to query.
|
||||
lock (taskAssignmentLock)
|
||||
{
|
||||
if (work == null)
|
||||
{
|
||||
pendingRequest = null;
|
||||
pendingRequestConsumedIDs = false;
|
||||
}
|
||||
else if (pendingRequest == null)
|
||||
{
|
||||
// special case for the first request ever.
|
||||
pendingRequest = Task.Run(work);
|
||||
pendingRequestConsumedIDs = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// append the new request on to the last to be executed.
|
||||
pendingRequest = pendingRequest.ContinueWith(_ => work());
|
||||
pendingRequestConsumedIDs = false;
|
||||
}
|
||||
pendingRequestTask = null;
|
||||
if (pendingUserTasks.Count > 0)
|
||||
createNewTask();
|
||||
}
|
||||
|
||||
return pendingRequest;
|
||||
foreach (var user in request.Result.Users)
|
||||
{
|
||||
if (userTasks.TryGetValue(user.Id, out var tasks))
|
||||
{
|
||||
foreach (var task in tasks)
|
||||
task.SetResult(user);
|
||||
|
||||
userTasks.Remove(user.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// if any tasks remain which were not satisfied, return null.
|
||||
foreach (var tasks in userTasks.Values)
|
||||
{
|
||||
foreach (var task in tasks)
|
||||
task.SetResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
/// <summary>
|
||||
/// Whether we want to expire triangles as they exit our draw area completely.
|
||||
/// </summary>
|
||||
[Obsolete("Unused.")] // Can be removed 20210518
|
||||
protected virtual bool ExpireOffScreenTriangles => true;
|
||||
|
||||
/// <summary>
|
||||
@ -86,12 +87,9 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
/// </summary>
|
||||
public float Velocity = 1;
|
||||
|
||||
private readonly Random stableRandom;
|
||||
|
||||
private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
|
||||
|
||||
private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default);
|
||||
|
||||
private Random stableRandom;
|
||||
private IShader shader;
|
||||
private readonly Texture texture;
|
||||
|
||||
@ -172,7 +170,20 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
}
|
||||
}
|
||||
|
||||
protected int AimCount;
|
||||
/// <summary>
|
||||
/// Clears and re-initialises triangles according to a given seed.
|
||||
/// </summary>
|
||||
/// <param name="seed">An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.</param>
|
||||
public void Reset(int? seed = null)
|
||||
{
|
||||
if (seed != null)
|
||||
stableRandom = new Random(seed.Value);
|
||||
|
||||
parts.Clear();
|
||||
addTriangles(true);
|
||||
}
|
||||
|
||||
protected int AimCount { get; private set; }
|
||||
|
||||
private void addTriangles(bool randomY)
|
||||
{
|
||||
@ -226,6 +237,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
}
|
||||
}
|
||||
|
||||
private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(this);
|
||||
|
||||
private class TrianglesDrawNode : DrawNode
|
||||
|
144
osu.Game/Graphics/ParticleExplosion.cs
Normal file
144
osu.Game/Graphics/ParticleExplosion.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -420,7 +420,7 @@ namespace osu.Game
|
||||
break;
|
||||
|
||||
case ScorePresentType.Results:
|
||||
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo));
|
||||
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
|
||||
break;
|
||||
}
|
||||
}, validScreens: new[] { typeof(PlaySongSelect) });
|
||||
|
@ -14,7 +14,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections.Beatmaps
|
||||
{
|
||||
public class PaginatedBeatmapContainer : PaginatedContainer<APIBeatmapSet>
|
||||
public class PaginatedBeatmapContainer : PaginatedProfileSubsection<APIBeatmapSet>
|
||||
{
|
||||
private const float panel_padding = 10f;
|
||||
private readonly BeatmapSetType type;
|
||||
|
@ -13,7 +13,7 @@ using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections.Historical
|
||||
{
|
||||
public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer<APIUserMostPlayedBeatmap>
|
||||
public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection<APIUserMostPlayedBeatmap>
|
||||
{
|
||||
public PaginatedMostPlayedBeatmapContainer(Bindable<User> user)
|
||||
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)
|
||||
|
@ -11,7 +11,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections.Kudosu
|
||||
{
|
||||
public class PaginatedKudosuHistoryContainer : PaginatedContainer<APIKudosuHistory>
|
||||
public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection<APIKudosuHistory>
|
||||
{
|
||||
public PaginatedKudosuHistoryContainer(Bindable<User> user)
|
||||
: base(user, missingText: "This user hasn't received any kudosu!")
|
||||
|
@ -6,62 +6,51 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Users;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections
|
||||
{
|
||||
public abstract class PaginatedContainer<TModel> : FillFlowContainer
|
||||
public abstract class PaginatedProfileSubsection<TModel> : ProfileSubsection
|
||||
{
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected RulesetStore Rulesets { get; private set; }
|
||||
|
||||
protected int VisiblePages;
|
||||
protected int ItemsPerPage;
|
||||
|
||||
protected readonly Bindable<User> User = new Bindable<User>();
|
||||
protected FillFlowContainer ItemsContainer;
|
||||
protected RulesetStore Rulesets;
|
||||
protected FillFlowContainer ItemsContainer { get; private set; }
|
||||
|
||||
private APIRequest<List<TModel>> retrievalRequest;
|
||||
private CancellationTokenSource loadCancellation;
|
||||
|
||||
private readonly string missingText;
|
||||
private ShowMoreButton moreButton;
|
||||
private OsuSpriteText missing;
|
||||
private PaginatedContainerHeader header;
|
||||
private readonly string missingText;
|
||||
|
||||
private readonly string headerText;
|
||||
private readonly CounterVisibilityState counterVisibilityState;
|
||||
|
||||
protected PaginatedContainer(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
|
||||
protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
|
||||
: base(user, headerText, counterVisibilityState)
|
||||
{
|
||||
this.headerText = headerText;
|
||||
this.missingText = missingText;
|
||||
this.counterVisibilityState = counterVisibilityState;
|
||||
User.BindTo(user);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Direction = FillDirection.Vertical;
|
||||
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
header = new PaginatedContainerHeader(headerText, counterVisibilityState)
|
||||
{
|
||||
Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
|
||||
},
|
||||
ItemsContainer = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@ -81,13 +70,14 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
Font = OsuFont.GetFont(size: 15),
|
||||
Text = missingText,
|
||||
Alpha = 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Rulesets = rulesets;
|
||||
|
||||
User.ValueChanged += onUserChanged;
|
||||
User.TriggerChange();
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
User.BindValueChanged(onUserChanged, true);
|
||||
}
|
||||
|
||||
private void onUserChanged(ValueChangedEvent<User> e)
|
||||
@ -124,7 +114,7 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
moreButton.Hide();
|
||||
moreButton.IsLoading = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(missing.Text))
|
||||
if (!string.IsNullOrEmpty(missingText))
|
||||
missing.Show();
|
||||
|
||||
return;
|
||||
@ -142,8 +132,6 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
|
||||
protected virtual int GetCount(User user) => 0;
|
||||
|
||||
protected void SetCount(int value) => header.Current.Value = value;
|
||||
|
||||
protected virtual void OnItemsReceived(List<TModel> items)
|
||||
{
|
||||
}
|
||||
@ -154,8 +142,9 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
retrievalRequest?.Cancel();
|
||||
loadCancellation?.Cancel();
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
51
osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
Normal file
51
osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections
|
||||
{
|
||||
public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue<int>
|
||||
public class ProfileSubsectionHeader : CompositeDrawable, IHasCurrentValue<int>
|
||||
{
|
||||
private readonly BindableWithCurrent<int> current = new BindableWithCurrent<int>();
|
||||
|
||||
@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
|
||||
private CounterPill counterPill;
|
||||
|
||||
public PaginatedContainerHeader(string text, CounterVisibilityState counterState)
|
||||
public ProfileSubsectionHeader(string text, CounterVisibilityState counterState)
|
||||
{
|
||||
this.text = text;
|
||||
this.counterState = counterState;
|
@ -14,7 +14,7 @@ using osu.Framework.Allocation;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections.Ranks
|
||||
{
|
||||
public class PaginatedScoreContainer : PaginatedContainer<APILegacyScoreInfo>
|
||||
public class PaginatedScoreContainer : PaginatedProfileSubsection<APILegacyScoreInfo>
|
||||
{
|
||||
private readonly ScoreType type;
|
||||
|
||||
|
@ -13,7 +13,7 @@ using osu.Framework.Allocation;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections.Recent
|
||||
{
|
||||
public class PaginatedRecentActivityContainer : PaginatedContainer<APIRecentActivity>
|
||||
public class PaginatedRecentActivityContainer : PaginatedProfileSubsection<APIRecentActivity>
|
||||
{
|
||||
public PaginatedRecentActivityContainer(Bindable<User> user)
|
||||
: base(user, missingText: "This user hasn't done anything notable recently!")
|
||||
|
75
osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
Normal file
75
osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs
Normal file
@ -0,0 +1,75 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
public class DefaultJudgementPiece : CompositeDrawable, IAnimatableJudgement
|
||||
{
|
||||
protected readonly HitResult Result;
|
||||
|
||||
protected SpriteText JudgementText { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public DefaultJudgementPiece(HitResult result)
|
||||
{
|
||||
Result = result;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
JudgementText = new OsuSpriteText
|
||||
{
|
||||
Text = Result.GetDescription().ToUpperInvariant(),
|
||||
Colour = colours.ForHitResult(Result),
|
||||
Font = OsuFont.Numeric.With(size: 20),
|
||||
Scale = new Vector2(0.85f, 1),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
this.ScaleTo(0.9f);
|
||||
this.ScaleTo(1, 500, Easing.OutElastic);
|
||||
break;
|
||||
}
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
}
|
||||
|
||||
public Drawable GetAboveHitObjectsProxiedContent() => null;
|
||||
}
|
||||
}
|
@ -1,20 +1,17 @@
|
||||
// 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.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osuTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
@ -25,25 +22,29 @@ namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
private const float judgement_size = 128;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public JudgementResult Result { get; private set; }
|
||||
|
||||
public DrawableHitObject JudgedObject { get; private set; }
|
||||
|
||||
protected Container JudgementBody { get; private set; }
|
||||
protected SpriteText JudgementText { get; private set; }
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
|
||||
private SkinnableDrawable bodyDrawable;
|
||||
protected SkinnableDrawable JudgementBody { get; private set; }
|
||||
|
||||
private readonly Container aboveHitObjectsContent;
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skinSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration of initial fade in.
|
||||
/// </summary>
|
||||
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
|
||||
protected virtual double FadeInDuration => 100;
|
||||
|
||||
/// <summary>
|
||||
/// Duration to wait until fade out begins. Defaults to <see cref="FadeInDuration"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
|
||||
protected virtual double FadeOutDelay => FadeInDuration;
|
||||
|
||||
/// <summary>
|
||||
@ -61,6 +62,12 @@ namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
Size = new Vector2(judgement_size);
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
AddInternal(aboveHitObjectsContent = new Container
|
||||
{
|
||||
Depth = float.MinValue,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -69,15 +76,61 @@ namespace osu.Game.Rulesets.Judgements
|
||||
prepareDrawables();
|
||||
}
|
||||
|
||||
protected virtual void ApplyHitAnimations()
|
||||
{
|
||||
JudgementBody.ScaleTo(0.9f);
|
||||
JudgementBody.ScaleTo(1, 500, Easing.OutElastic);
|
||||
public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
|
||||
|
||||
this.Delay(FadeOutDelay).FadeOut(400);
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
skinSource.SourceChanged += onSkinChanged;
|
||||
}
|
||||
|
||||
public virtual void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
|
||||
private void onSkinChanged()
|
||||
{
|
||||
// on a skin change, the child component will update but not get correctly triggered to play its animation.
|
||||
// we need to trigger a reinitialisation to make things right.
|
||||
currentDrawableType = null;
|
||||
|
||||
PrepareForUse();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skinSource != null)
|
||||
skinSource.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply top-level animations to the current judgement when successfully hit.
|
||||
/// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For animating the actual "default skin" judgement itself, it is recommended to use <see cref="CreateDefaultJudgement"/>.
|
||||
/// This allows applying animations which don't affect custom skins.
|
||||
/// </remarks>
|
||||
protected virtual void ApplyHitAnimations()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply top-level animations to the current judgement when missed.
|
||||
/// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For animating the actual "default skin" judgement itself, it is recommended to use <see cref="CreateDefaultJudgement"/>.
|
||||
/// This allows applying animations which don't affect custom skins.
|
||||
/// </remarks>
|
||||
protected virtual void ApplyMissAnimations()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Associate a new result / object with this judgement. Should be called when retrieving a judgement from a pool.
|
||||
/// </summary>
|
||||
/// <param name="result">The applicable judgement.</param>
|
||||
/// <param name="judgedObject">The drawable object.</param>
|
||||
public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject)
|
||||
{
|
||||
Result = result;
|
||||
JudgedObject = judgedObject;
|
||||
@ -91,34 +144,46 @@ namespace osu.Game.Rulesets.Judgements
|
||||
|
||||
prepareDrawables();
|
||||
|
||||
bodyDrawable.ResetAnimation();
|
||||
runAnimation();
|
||||
}
|
||||
|
||||
this.FadeInFromZero(FadeInDuration, Easing.OutQuint);
|
||||
JudgementBody.ScaleTo(1);
|
||||
JudgementBody.RotateTo(0);
|
||||
JudgementBody.MoveTo(Vector2.Zero);
|
||||
private void runAnimation()
|
||||
{
|
||||
ClearTransforms(true);
|
||||
LifetimeStart = Result.TimeAbsolute;
|
||||
|
||||
switch (Result.Type)
|
||||
using (BeginAbsoluteSequence(Result.TimeAbsolute, true))
|
||||
{
|
||||
case HitResult.None:
|
||||
break;
|
||||
// not sure if this should remain going forward.
|
||||
JudgementBody.ResetAnimation();
|
||||
|
||||
case HitResult.Miss:
|
||||
JudgementBody.ScaleTo(1.6f);
|
||||
JudgementBody.ScaleTo(1, 100, Easing.In);
|
||||
switch (Result.Type)
|
||||
{
|
||||
case HitResult.None:
|
||||
break;
|
||||
|
||||
JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
JudgementBody.RotateTo(40, 800, Easing.InQuint);
|
||||
case HitResult.Miss:
|
||||
ApplyMissAnimations();
|
||||
break;
|
||||
|
||||
this.Delay(600).FadeOut(200);
|
||||
break;
|
||||
default:
|
||||
ApplyHitAnimations();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
ApplyHitAnimations();
|
||||
break;
|
||||
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
|
||||
{
|
||||
var drawableAnimation = (Drawable)animatable;
|
||||
|
||||
animatable.PlayAnimation();
|
||||
|
||||
// a derived version of DrawableJudgement may be proposing a lifetime.
|
||||
// if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
|
||||
double lastTransformTime = drawableAnimation.LatestTransformEndTime;
|
||||
if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
|
||||
LifetimeEnd = lastTransformTime;
|
||||
}
|
||||
}
|
||||
|
||||
Expire(true);
|
||||
}
|
||||
|
||||
private HitResult? currentDrawableType;
|
||||
@ -127,6 +192,7 @@ namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset
|
||||
|
||||
// todo: this should be removed once judgements are always pooled.
|
||||
if (type == currentDrawableType)
|
||||
return;
|
||||
|
||||
@ -134,21 +200,24 @@ namespace osu.Game.Rulesets.Judgements
|
||||
if (JudgementBody != null)
|
||||
RemoveInternal(JudgementBody);
|
||||
|
||||
AddInternal(JudgementBody = new Container
|
||||
aboveHitObjectsContent.Clear();
|
||||
AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ =>
|
||||
CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = bodyDrawable = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(type), _ => JudgementText = new OsuSpriteText
|
||||
{
|
||||
Text = type.GetDescription().ToUpperInvariant(),
|
||||
Font = OsuFont.Numeric.With(size: 20),
|
||||
Colour = colours.ForHitResult(type),
|
||||
Scale = new Vector2(0.85f, 1),
|
||||
}, confineMode: ConfineMode.NoScaling)
|
||||
});
|
||||
|
||||
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
|
||||
{
|
||||
var proxiedContent = animatable.GetAboveHitObjectsProxiedContent();
|
||||
if (proxiedContent != null)
|
||||
aboveHitObjectsContent.Add(proxiedContent);
|
||||
}
|
||||
|
||||
currentDrawableType = type;
|
||||
}
|
||||
|
||||
protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result);
|
||||
}
|
||||
}
|
||||
|
25
osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs
Normal file
25
osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
/// <summary>
|
||||
/// A skinnable judgement element which supports playing an animation from the current point in time.
|
||||
/// </summary>
|
||||
public interface IAnimatableJudgement : IDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Start the animation for this judgement from the current point in time.
|
||||
/// </summary>
|
||||
void PlayAnimation();
|
||||
|
||||
/// <summary>
|
||||
/// Get proxied content which should be displayed above all hitobjects.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
Drawable GetAboveHitObjectsProxiedContent();
|
||||
}
|
||||
}
|
@ -10,14 +10,6 @@ namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
public static class SliderEventGenerator
|
||||
{
|
||||
[Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115
|
||||
// ReSharper disable once RedundantOverload.Global
|
||||
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
|
||||
double? legacyLastTickOffset)
|
||||
{
|
||||
return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default);
|
||||
}
|
||||
|
||||
// ReSharper disable once MethodOverloadWithOptionalParameter
|
||||
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
|
||||
double? legacyLastTickOffset, CancellationToken cancellationToken = default)
|
||||
|
@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() =>
|
||||
new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both };
|
||||
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
|
||||
@ -338,7 +337,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <returns>Whether a selection was performed.</returns>
|
||||
private bool beginClickSelection(MouseButtonEvent e)
|
||||
{
|
||||
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
|
||||
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
|
||||
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse())
|
||||
{
|
||||
if (!blueprint.IsHovered) continue;
|
||||
|
||||
|
@ -0,0 +1,77 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A container for <see cref="SelectionBlueprint"/> ordered by their <see cref="HitObject"/> start times.
|
||||
/// </summary>
|
||||
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint>
|
||||
{
|
||||
public override void Add(SelectionBlueprint drawable)
|
||||
{
|
||||
base.Add(drawable);
|
||||
bindStartTime(drawable);
|
||||
}
|
||||
|
||||
public override bool Remove(SelectionBlueprint drawable)
|
||||
{
|
||||
if (!base.Remove(drawable))
|
||||
return false;
|
||||
|
||||
unbindStartTime(drawable);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Clear(bool disposeChildren)
|
||||
{
|
||||
base.Clear(disposeChildren);
|
||||
unbindAllStartTimes();
|
||||
}
|
||||
|
||||
private readonly Dictionary<SelectionBlueprint, IBindable> startTimeMap = new Dictionary<SelectionBlueprint, IBindable>();
|
||||
|
||||
private void bindStartTime(SelectionBlueprint blueprint)
|
||||
{
|
||||
var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy();
|
||||
|
||||
bindable.BindValueChanged(_ =>
|
||||
{
|
||||
if (LoadState >= LoadState.Ready)
|
||||
SortInternal();
|
||||
});
|
||||
|
||||
startTimeMap[blueprint] = bindable;
|
||||
}
|
||||
|
||||
private void unbindStartTime(SelectionBlueprint blueprint)
|
||||
{
|
||||
startTimeMap[blueprint].UnbindAll();
|
||||
startTimeMap.Remove(blueprint);
|
||||
}
|
||||
|
||||
private void unbindAllStartTimes()
|
||||
{
|
||||
foreach (var kvp in startTimeMap)
|
||||
kvp.Value.UnbindAll();
|
||||
startTimeMap.Clear();
|
||||
}
|
||||
|
||||
protected override int Compare(Drawable x, Drawable y)
|
||||
{
|
||||
var xObj = (SelectionBlueprint)x;
|
||||
var yObj = (SelectionBlueprint)y;
|
||||
|
||||
// Put earlier blueprints towards the end of the list, so they handle input first
|
||||
int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
|
||||
return i == 0 ? CompareReverseChildID(x, y) : i;
|
||||
}
|
||||
}
|
||||
}
|
@ -201,7 +201,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
public TimelineSelectionBlueprintContainer()
|
||||
{
|
||||
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
|
||||
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,8 @@ namespace osu.Game.Screens.Edit.Compose
|
||||
composer = ruleset?.CreateHitObjectComposer();
|
||||
|
||||
// make the composer available to the timeline and other components in this screen.
|
||||
dependencies.CacheAs(composer);
|
||||
if (composer != null)
|
||||
dependencies.CacheAs(composer);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Screens.Multi.Play
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score)
|
||||
{
|
||||
Debug.Assert(roomId.Value != null);
|
||||
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem);
|
||||
return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true);
|
||||
}
|
||||
|
||||
protected override ScoreInfo CreateScore()
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Ranking
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
|
||||
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry)
|
||||
: base(score, allowRetry)
|
||||
{
|
||||
this.roomId = roomId;
|
||||
|
@ -545,7 +545,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
|
||||
|
||||
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score);
|
||||
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
|
||||
|
||||
#region Fail Logic
|
||||
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play
|
||||
public class SpectatorResultsScreen : SoloResultsScreen
|
||||
{
|
||||
public SpectatorResultsScreen(ScoreInfo score)
|
||||
: base(score)
|
||||
: base(score, false)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -57,7 +58,7 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
private readonly bool allowRetry;
|
||||
|
||||
protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
|
||||
protected ResultsScreen(ScoreInfo score, bool allowRetry)
|
||||
{
|
||||
Score = score;
|
||||
this.allowRetry = allowRetry;
|
||||
@ -149,7 +150,12 @@ namespace osu.Game.Screens.Ranking
|
||||
};
|
||||
|
||||
if (Score != null)
|
||||
ScorePanelList.AddScore(Score, true);
|
||||
{
|
||||
// only show flair / animation when arriving after watching a play that isn't autoplay.
|
||||
bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay);
|
||||
|
||||
ScorePanelList.AddScore(Score, shouldFlair);
|
||||
}
|
||||
|
||||
if (player != null && allowRetry)
|
||||
{
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
public SoloResultsScreen(ScoreInfo score, bool allowRetry = true)
|
||||
public SoloResultsScreen(ScoreInfo score, bool allowRetry)
|
||||
: base(score, allowRetry)
|
||||
{
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
|
||||
protected void PresentScore(ScoreInfo score) =>
|
||||
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score)));
|
||||
FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
|
||||
|
||||
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
|
||||
|
||||
|
127
osu.Game/Skinning/LegacyJudgementPieceNew.cs
Normal file
127
osu.Game/Skinning/LegacyJudgementPieceNew.cs
Normal file
@ -0,0 +1,127 @@
|
||||
// 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.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class LegacyJudgementPieceNew : CompositeDrawable, IAnimatableJudgement
|
||||
{
|
||||
private readonly HitResult result;
|
||||
|
||||
private readonly LegacyJudgementPieceOld temporaryOldStyle;
|
||||
|
||||
private readonly Drawable mainPiece;
|
||||
|
||||
private readonly ParticleExplosion particles;
|
||||
|
||||
public LegacyJudgementPieceNew(HitResult result, Func<Drawable> createMainDrawable, Texture particleTexture)
|
||||
{
|
||||
this.result = result;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
mainPiece = createMainDrawable().With(d =>
|
||||
{
|
||||
d.Anchor = Anchor.Centre;
|
||||
d.Origin = Anchor.Centre;
|
||||
})
|
||||
};
|
||||
|
||||
if (particleTexture != null)
|
||||
{
|
||||
AddInternal(particles = new ParticleExplosion(particleTexture, 150, 1600)
|
||||
{
|
||||
Size = new Vector2(140),
|
||||
Depth = float.MaxValue,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
|
||||
if (result != HitResult.Miss)
|
||||
{
|
||||
//new judgement shows old as a temporary effect
|
||||
AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f)
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayAnimation()
|
||||
{
|
||||
var animation = mainPiece as IFramedAnimation;
|
||||
|
||||
animation?.GotoFrame(0);
|
||||
|
||||
if (particles != null)
|
||||
{
|
||||
// start the particles already some way into their animation to break cluster away from centre.
|
||||
using (particles.BeginDelayedSequence(-100, true))
|
||||
particles.Restart();
|
||||
}
|
||||
|
||||
const double fade_in_length = 120;
|
||||
const double fade_out_delay = 500;
|
||||
const double fade_out_length = 600;
|
||||
|
||||
this.FadeInFromZero(fade_in_length);
|
||||
this.Delay(fade_out_delay).FadeOut(fade_out_length);
|
||||
|
||||
// new style non-miss judgements show the original style temporarily, with additive colour.
|
||||
if (temporaryOldStyle != null)
|
||||
{
|
||||
temporaryOldStyle.PlayAnimation();
|
||||
|
||||
temporaryOldStyle.Hide();
|
||||
temporaryOldStyle.Delay(-16)
|
||||
.FadeTo(0.5f, 56, Easing.Out).Then()
|
||||
.FadeOut(300);
|
||||
}
|
||||
|
||||
// legacy judgements don't play any transforms if they are an animation.
|
||||
if (animation?.FrameCount > 1)
|
||||
return;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
//todo: this only applies to osu! ruleset apparently.
|
||||
this.MoveTo(new Vector2(0, -2));
|
||||
this.MoveToOffset(new Vector2(0, 20), fade_out_delay + fade_out_length, Easing.In);
|
||||
|
||||
float rotation = RNG.NextSingle(-8.6f, 8.6f);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(rotation, fade_in_length)
|
||||
.Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
|
||||
break;
|
||||
|
||||
default:
|
||||
mainPiece.ScaleTo(0.9f);
|
||||
mainPiece.ScaleTo(1.05f, fade_out_delay + fade_out_length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable GetAboveHitObjectsProxiedContent() => temporaryOldStyle?.CreateProxy(); // for new style judgements, only the old style temporary display is in front of objects.
|
||||
}
|
||||
}
|
75
osu.Game/Skinning/LegacyJudgementPieceOld.cs
Normal file
75
osu.Game/Skinning/LegacyJudgementPieceOld.cs
Normal file
@ -0,0 +1,75 @@
|
||||
// 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.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class LegacyJudgementPieceOld : CompositeDrawable, IAnimatableJudgement
|
||||
{
|
||||
private readonly HitResult result;
|
||||
|
||||
private readonly float finalScale;
|
||||
|
||||
public LegacyJudgementPieceOld(HitResult result, Func<Drawable> createMainDrawable, float finalScale = 1f)
|
||||
{
|
||||
this.result = result;
|
||||
this.finalScale = finalScale;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = createMainDrawable();
|
||||
}
|
||||
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
var animation = InternalChild as IFramedAnimation;
|
||||
|
||||
animation?.GotoFrame(0);
|
||||
|
||||
const double fade_in_length = 120;
|
||||
const double fade_out_delay = 500;
|
||||
const double fade_out_length = 600;
|
||||
|
||||
this.FadeInFromZero(fade_in_length);
|
||||
this.Delay(fade_out_delay).FadeOut(fade_out_length);
|
||||
|
||||
// legacy judgements don't play any transforms if they are an animation.
|
||||
if (animation?.FrameCount > 1)
|
||||
return;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
float rotation = RNG.NextSingle(-8.6f, 8.6f);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(rotation, fade_in_length)
|
||||
.Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
this.ScaleTo(0.6f).Then()
|
||||
.ScaleTo(1.1f, fade_in_length * 0.8f).Then()
|
||||
// this is actually correct to match stable; there were overlapping transforms.
|
||||
.ScaleTo(0.9f).Delay(fade_in_length * 0.2f)
|
||||
.ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
|
||||
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
|
||||
}
|
||||
}
|
@ -371,19 +371,15 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
|
||||
case GameplaySkinComponent<HitResult> resultComponent:
|
||||
switch (resultComponent.Component)
|
||||
Func<Drawable> createDrawable = () => getJudgementAnimation(resultComponent.Component);
|
||||
|
||||
// kind of wasteful that we throw this away, but should do for now.
|
||||
if (createDrawable() != null)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
return this.GetAnimation("hit0", true, false);
|
||||
|
||||
case HitResult.Meh:
|
||||
return this.GetAnimation("hit50", true, false);
|
||||
|
||||
case HitResult.Ok:
|
||||
return this.GetAnimation("hit100", true, false);
|
||||
|
||||
case HitResult.Great:
|
||||
return this.GetAnimation("hit300", true, false);
|
||||
if (Configuration.LegacyVersion > 1)
|
||||
return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component));
|
||||
else
|
||||
return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -392,6 +388,43 @@ namespace osu.Game.Skinning
|
||||
return this.GetAnimation(component.LookupName, false, false);
|
||||
}
|
||||
|
||||
private Texture getParticleTexture(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Meh:
|
||||
return GetTexture("particle50");
|
||||
|
||||
case HitResult.Ok:
|
||||
return GetTexture("particle100");
|
||||
|
||||
case HitResult.Great:
|
||||
return GetTexture("particle300");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Drawable getJudgementAnimation(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
return this.GetAnimation("hit0", true, false);
|
||||
|
||||
case HitResult.Meh:
|
||||
return this.GetAnimation("hit50", true, false);
|
||||
|
||||
case HitResult.Ok:
|
||||
return this.GetAnimation("hit100", true, false);
|
||||
|
||||
case HitResult.Great:
|
||||
return this.GetAnimation("hit300", true, false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
{
|
||||
foreach (var name in getFallbackNames(componentName))
|
||||
|
@ -24,7 +24,15 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
}
|
||||
|
||||
protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) };
|
||||
protected override Drawable CreateDefault(ISkinComponent component)
|
||||
{
|
||||
var texture = textures.Get(component.LookupName);
|
||||
|
||||
if (texture == null)
|
||||
return null;
|
||||
|
||||
return new Sprite { Texture = texture };
|
||||
}
|
||||
|
||||
private class SpriteComponent : ISkinComponent
|
||||
{
|
||||
|
@ -26,7 +26,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1120.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
|
||||
<PackageReference Include="Sentry" Version="2.1.6" />
|
||||
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
||||
|
@ -70,7 +70,7 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1113.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1120.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||
@ -88,7 +88,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.1120.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user