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

Merge branch 'master' into mania-legacyskin-scoreposition

This commit is contained in:
smoogipoo 2020-12-14 13:10:48 +09:00
commit e01b41b7d1
31 changed files with 520 additions and 318 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1203.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1212.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -18,6 +18,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
private Container droppedObjectContainer; private Container<CaughtObject> droppedObjectContainer;
private TestCatcher catcher; private TestCatcher catcher;
@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}; };
var trailContainer = new Container(); var trailContainer = new Container();
droppedObjectContainer = new Container(); droppedObjectContainer = new Container<CaughtObject>();
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty); catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
Child = new Container Child = new Container
@ -65,13 +66,11 @@ namespace osu.Game.Rulesets.Catch.Tests
JudgementResult result2 = null; JudgementResult result2 = null;
AddStep("catch hyper fruit", () => AddStep("catch hyper fruit", () =>
{ {
drawableObject1 = createDrawableObject(new Fruit { HyperDashTarget = new Fruit { X = 100 } }); attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1);
result1 = attemptCatch(drawableObject1);
}); });
AddStep("catch normal fruit", () => AddStep("catch normal fruit", () =>
{ {
drawableObject2 = createDrawableObject(new Fruit()); attemptCatch(new Fruit(), out drawableObject2, out result2);
result2 = attemptCatch(drawableObject2);
}); });
AddStep("revert second result", () => AddStep("revert second result", () =>
{ {
@ -92,8 +91,7 @@ namespace osu.Game.Rulesets.Catch.Tests
JudgementResult result = null; JudgementResult result = null;
AddStep("catch kiai fruit", () => AddStep("catch kiai fruit", () =>
{ {
drawableObject = createDrawableObject(new TestKiaiFruit()); attemptCatch(new TestKiaiFruit(), out drawableObject, out result);
result = attemptCatch(drawableObject);
}); });
checkState(CatcherAnimationState.Kiai); checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () => AddStep("revert result", () =>
@ -200,13 +198,22 @@ namespace osu.Game.Rulesets.Catch.Tests
AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
} }
[TestCase(true)] [Test]
[TestCase(false)] public void TestHitLightingColour()
public void TestHitLighting(bool enabled)
{ {
AddStep($"{(enabled ? "enable" : "disable")} hit lighting", () => config.Set(OsuSetting.HitLighting, enabled)); var fruitColour = SkinConfiguration.DefaultComboColours[1];
AddStep("enable hit lighting", () => config.Set(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit())); AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("check hit lighting", () => catcher.ChildrenOfType<HitExplosion>().Any() == enabled); AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType<HitExplosion>().First()?.ObjectColour == fruitColour);
}
[Test]
public void TestHitLightingDisabled()
{
AddStep("disable hit lighting", () => config.Set(OsuSetting.HitLighting, false));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
} }
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count); private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
@ -218,18 +225,34 @@ namespace osu.Game.Rulesets.Catch.Tests
private void attemptCatch(CatchHitObject hitObject, int count = 1) private void attemptCatch(CatchHitObject hitObject, int count = 1)
{ {
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
attemptCatch(createDrawableObject(hitObject)); attemptCatch(hitObject, out _, out _);
} }
private JudgementResult attemptCatch(DrawableCatchHitObject drawableObject) private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result)
{ {
drawableObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var result = new CatchJudgementResult(drawableObject.HitObject, drawableObject.HitObject.CreateJudgement()) drawableObject = createDrawableObject(hitObject);
result = createResult(hitObject);
applyResult(drawableObject, result);
}
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
// Load DHO to set colour of hit explosion correctly
Add(drawableObject);
drawableObject.OnLoadComplete += _ =>
{ {
Type = catcher.CanCatch(drawableObject.HitObject) ? HitResult.Great : HitResult.Miss catcher.OnNewResult(drawableObject, result);
drawableObject.Expire();
};
}
private JudgementResult createResult(CatchHitObject hitObject)
{
return new CatchJudgementResult(hitObject, hitObject.CreateJudgement())
{
Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss
}; };
catcher.OnNewResult(drawableObject, result);
return result;
} }
private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject) private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject)
@ -252,9 +275,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestCatcher : Catcher public class TestCatcher : Catcher
{ {
public IEnumerable<DrawablePalpableCatchHitObject> CaughtObjects => this.ChildrenOfType<DrawablePalpableCatchHitObject>(); public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty) public TestCatcher(Container trailsTarget, Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, droppedObjectTarget, difficulty) : base(trailsTarget, droppedObjectTarget, difficulty)
{ {
} }

View File

@ -73,7 +73,10 @@ namespace osu.Game.Rulesets.Catch.Tests
SetContents(() => SetContents(() =>
{ {
var droppedObjectContainer = new Container(); var droppedObjectContainer = new Container<CaughtObject>
{
RelativeSizeAxes = Axes.Both
};
return new CatchInputManager(catchRuleset) return new CatchInputManager(catchRuleset)
{ {
@ -99,7 +102,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea private class TestCatcherArea : CatcherArea
{ {
public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty) public TestCatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
: base(droppedObjectContainer, beatmapDifficulty) : base(droppedObjectContainer, beatmapDifficulty)
{ {
} }

View File

@ -5,19 +5,20 @@ using NUnit.Framework;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
public class TestSceneFruitRandomness : OsuTestScene public class TestSceneFruitRandomness : OsuTestScene
{ {
private readonly TestDrawableFruit drawableFruit; private readonly DrawableFruit drawableFruit;
private readonly TestDrawableBanana drawableBanana; private readonly DrawableBanana drawableBanana;
public TestSceneFruitRandomness() public TestSceneFruitRandomness()
{ {
drawableFruit = new TestDrawableFruit(new Fruit()); drawableFruit = new DrawableFruit(new Fruit());
drawableBanana = new TestDrawableBanana(new Banana()); drawableBanana = new DrawableBanana(new Banana());
Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 });
Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana));
@ -37,16 +38,16 @@ namespace osu.Game.Rulesets.Catch.Tests
float fruitRotation = 0; float fruitRotation = 0;
float bananaRotation = 0; float bananaRotation = 0;
float bananaScale = 0; Vector2 bananaSize = new Vector2();
Color4 bananaColour = new Color4(); Color4 bananaColour = new Color4();
AddStep("Initialize start time", () => AddStep("Initialize start time", () =>
{ {
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
fruitRotation = drawableFruit.InnerRotation; fruitRotation = drawableFruit.DisplayRotation;
bananaRotation = drawableBanana.InnerRotation; bananaRotation = drawableBanana.DisplayRotation;
bananaScale = drawableBanana.InnerScale; bananaSize = drawableBanana.DisplaySize;
bananaColour = drawableBanana.AccentColour.Value; bananaColour = drawableBanana.AccentColour.Value;
}); });
@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Catch.Tests
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time;
}); });
AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation);
AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation); AddAssert("banana rotation is changed", () => drawableBanana.DisplayRotation != bananaRotation);
AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); AddAssert("banana size is changed", () => drawableBanana.DisplaySize != bananaSize);
AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour); AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour);
AddStep("reset start time", () => AddStep("reset start time", () =>
@ -65,32 +66,11 @@ namespace osu.Game.Rulesets.Catch.Tests
drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
}); });
AddAssert("rotation and scale restored", () => AddAssert("rotation and size restored", () =>
drawableFruit.InnerRotation == fruitRotation && drawableFruit.DisplayRotation == fruitRotation &&
drawableBanana.InnerRotation == bananaRotation && drawableBanana.DisplayRotation == bananaRotation &&
drawableBanana.InnerScale == bananaScale && drawableBanana.DisplaySize == bananaSize &&
drawableBanana.AccentColour.Value == bananaColour); drawableBanana.AccentColour.Value == bananaColour);
} }
private class TestDrawableFruit : DrawableFruit
{
public float InnerRotation => ScaleContainer.Rotation;
public TestDrawableFruit(Fruit h)
: base(h)
{
}
}
private class TestDrawableBanana : DrawableBanana
{
public float InnerRotation => ScaleContainer.Rotation;
public float InnerScale => ScaleContainer.Scale.X;
public TestDrawableBanana(Banana h)
: base(h)
{
}
}
} }
} }

View File

@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("create hyper-dashing catcher", () => AddStep("create hyper-dashing catcher", () =>
{ {
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container()) Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container<CaughtObject>())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -0,0 +1,18 @@
// 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.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="Banana"/> caught by the catcher.
/// </summary>
public class CaughtBanana : CaughtObject
{
public CaughtBanana()
: base(CatchSkinComponents.Banana, _ => new BananaPiece())
{
}
}
}

View File

@ -0,0 +1,20 @@
// 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.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="Droplet"/> caught by the catcher.
/// </summary>
public class CaughtDroplet : CaughtObject
{
public override bool StaysOnPlate => false;
public CaughtDroplet()
: base(CatchSkinComponents.Droplet, _ => new DropletPiece())
{
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Catch.Skinning.Default;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="Fruit"/> caught by the catcher.
/// </summary>
public class CaughtFruit : CaughtObject, IHasFruitState
{
public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public CaughtFruit()
: base(CatchSkinComponents.Fruit, _ => new FruitPiece())
{
}
public override void CopyStateFrom(IHasCatchObjectState objectState)
{
base.CopyStateFrom(objectState);
var fruitState = (IHasFruitState)objectState;
VisualRepresentation.Value = fruitState.VisualRepresentation.Value;
}
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Represents a <see cref="PalpableCatchHitObject"/> caught by the catcher.
/// </summary>
[Cached(typeof(IHasCatchObjectState))]
public abstract class CaughtObject : SkinnableDrawable, IHasCatchObjectState
{
public PalpableCatchHitObject HitObject { get; private set; }
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public Vector2 DisplaySize => Size * Scale;
public float DisplayRotation => Rotation;
/// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
/// </summary>
public virtual bool StaysOnPlate => true;
public override bool RemoveWhenNotAlive => true;
protected CaughtObject(CatchSkinComponents skinComponent, Func<ISkinComponent, Drawable> defaultImplementation)
: base(new CatchSkinComponent(skinComponent), defaultImplementation)
{
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.None;
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
}
/// <summary>
/// Copies the hit object visual state from another <see cref="IHasCatchObjectState"/> object.
/// </summary>
public virtual void CopyStateFrom(IHasCatchObjectState objectState)
{
HitObject = objectState.HitObject;
Scale = Vector2.Divide(objectState.DisplaySize, Size);
Rotation = objectState.DisplayRotation;
AccentColour.Value = objectState.AccentColour.Value;
HyperDash.Value = objectState.HyperDash.Value;
}
protected override void FreeAfterUse()
{
ClearTransforms();
Alpha = 1;
base.FreeAfterUse();
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable( ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Banana), new CatchSkinComponent(CatchSkinComponents.Banana),
_ => new BananaPiece()); _ => new BananaPiece());
} }
@ -44,12 +44,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
const float end_scale = 0.6f; const float end_scale = 0.6f;
const float random_scale_range = 1.6f; const float random_scale_range = 1.6f;
ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
ScaleContainer.RotateTo(getRandomAngle(1)) ScalingContainer.RotateTo(getRandomAngle(1))
.Then() .Then()
.RotateTo(getRandomAngle(2), HitObject.TimePreempt); .RotateTo(getRandomAngle(2), HitObject.TimePreempt);
float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1); float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
} }

View File

@ -50,10 +50,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public Func<CatchHitObject, bool> CheckPosition; public Func<CatchHitObject, bool> CheckPosition;
public bool IsOnPlate;
public override bool RemoveWhenNotAlive => IsOnPlate;
protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement); protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement);
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)

View File

@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableDroplet : DrawablePalpableCatchHitObject public class DrawableDroplet : DrawablePalpableCatchHitObject
{ {
public override bool StaysOnPlate => false;
public DrawableDroplet() public DrawableDroplet()
: this(null) : this(null)
{ {
@ -26,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
ScaleContainer.Child = new SkinnableDrawable( ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Droplet), new CatchSkinComponent(CatchSkinComponents.Droplet),
_ => new DropletPiece()); _ => new DropletPiece());
} }
@ -39,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
float startRotation = RandomSingle(1) * 20; float startRotation = RandomSingle(1) * 20;
double duration = HitObject.TimePreempt + 2000; double duration = HitObject.TimePreempt + 2000;
ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration); ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
} }
} }
} }

View File

@ -10,9 +10,9 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public class DrawableFruit : DrawablePalpableCatchHitObject public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState
{ {
public readonly Bindable<FruitVisualRepresentation> VisualRepresentation = new Bindable<FruitVisualRepresentation>(); public Bindable<FruitVisualRepresentation> VisualRepresentation { get; } = new Bindable<FruitVisualRepresentation>();
public DrawableFruit() public DrawableFruit()
: this(null) : this(null)
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4); VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4);
}, true); }, true);
ScaleContainer.Child = new SkinnableDrawable( ScalingContainer.Child = new SkinnableDrawable(
new CatchSkinComponent(CatchSkinComponents.Fruit), new CatchSkinComponent(CatchSkinComponents.Fruit),
_ => new FruitPiece()); _ => new FruitPiece());
} }
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();
ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
} }
} }

View File

@ -7,32 +7,36 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject [Cached(typeof(IHasCatchObjectState))]
public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject, IHasCatchObjectState
{ {
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
public readonly Bindable<bool> HyperDash = new Bindable<bool>(); Bindable<Color4> IHasCatchObjectState.AccentColour => AccentColour;
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1); public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public readonly Bindable<int> IndexInBeatmap = new Bindable<int>(); public Bindable<float> ScaleBindable { get; } = new Bindable<float>(1);
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
/// <summary> /// <summary>
/// The multiplicative factor applied to <see cref="ScaleContainer"/> scale relative to <see cref="HitObject"/> scale. /// The multiplicative factor applied to <see cref="Drawable.Scale"/> relative to <see cref="HitObject"/> scale.
/// </summary> /// </summary>
protected virtual float ScaleFactor => 1; protected virtual float ScaleFactor => 1;
/// <summary> /// <summary>
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. /// The container internal transforms (such as scaling based on the circle size) are applied to.
/// </summary> /// </summary>
public virtual bool StaysOnPlate => true; protected readonly Container ScalingContainer;
public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
protected readonly Container ScaleContainer; public float DisplayRotation => ScalingContainer.Rotation;
protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)
: base(h) : base(h)
@ -40,11 +44,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Origin = Anchor.Centre; Origin = Anchor.Centre;
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
AddInternal(ScaleContainer = new Container AddInternal(ScalingContainer = new Container
{ {
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2)
}); });
} }
@ -53,12 +57,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{ {
XBindable.BindValueChanged(x => XBindable.BindValueChanged(x =>
{ {
if (!IsOnPlate) X = x.NewValue; X = x.NewValue;
}, true); }, true);
ScaleBindable.BindValueChanged(scale => ScaleBindable.BindValueChanged(scale =>
{ {
ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor); ScalingContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
Size = DisplaySize;
}, true); }, true);
IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());

View 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 osu.Framework.Bindables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Provides a visual state of a <see cref="PalpableCatchHitObject"/>.
/// </summary>
public interface IHasCatchObjectState
{
PalpableCatchHitObject HitObject { get; }
Bindable<Color4> AccentColour { get; }
Bindable<bool> HyperDash { get; }
Vector2 DisplaySize { get; }
float DisplayRotation { get; }
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
/// <summary>
/// Provides a visual state of a <see cref="Fruit"/>.
/// </summary>
public interface IHasFruitState : IHasCatchObjectState
{
Bindable<FruitVisualRepresentation> VisualRepresentation { get; }
}
}

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Default namespace osu.Game.Rulesets.Catch.Skinning.Default
@ -17,9 +16,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(); public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly Bindable<bool> HyperDash = new Bindable<bool>(); public readonly Bindable<bool> HyperDash = new Bindable<bool>();
[Resolved(canBeNull: true)] [Resolved]
[CanBeNull] protected IHasCatchObjectState ObjectState { get; private set; }
protected DrawableHitObject DrawableHitObject { get; private set; }
/// <summary> /// <summary>
/// A part of this piece that will be faded out while falling in the playfield. /// A part of this piece that will be faded out while falling in the playfield.
@ -37,13 +35,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
base.LoadComplete(); base.LoadComplete();
var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject; AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
if (hitObject != null)
{
AccentColour.BindTo(hitObject.AccentColour);
HyperDash.BindTo(hitObject.HyperDash);
}
HyperDash.BindValueChanged(hyper => HyperDash.BindValueChanged(hyper =>
{ {
@ -54,8 +47,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
protected override void Update() protected override void Update()
{ {
if (BorderPiece != null && DrawableHitObject?.HitObject != null) if (BorderPiece != null)
BorderPiece.Alpha = (float)Math.Clamp((DrawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); BorderPiece.Alpha = (float)Math.Clamp((ObjectState.HitObject.StartTime - Time.Current) / 500, 0, 1);
} }
} }
} }

View File

@ -39,10 +39,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{ {
base.LoadComplete(); base.LoadComplete();
var fruit = (DrawableFruit)DrawableHitObject; var fruitState = (IHasFruitState)ObjectState;
VisualRepresentation.BindTo(fruitState.VisualRepresentation);
if (fruit != null)
VisualRepresentation.BindTo(fruit.VisualRepresentation);
} }
} }
} }

View File

@ -14,10 +14,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
base.LoadComplete(); base.LoadComplete();
var fruit = (DrawableFruit)DrawableHitObject; var fruitState = (IHasFruitState)ObjectState;
VisualRepresentation.BindTo(fruitState.VisualRepresentation);
if (fruit != null)
VisualRepresentation.BindTo(fruit.VisualRepresentation);
VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true); VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true);
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,7 +9,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -29,9 +27,8 @@ namespace osu.Game.Rulesets.Catch.Skinning
[Resolved] [Resolved]
protected ISkinSource Skin { get; private set; } protected ISkinSource Skin { get; private set; }
[Resolved(canBeNull: true)] [Resolved]
[CanBeNull] protected IHasCatchObjectState ObjectState { get; private set; }
protected DrawableHitObject DrawableHitObject { get; private set; }
protected LegacyCatchHitObjectPiece() protected LegacyCatchHitObjectPiece()
{ {
@ -65,13 +62,8 @@ namespace osu.Game.Rulesets.Catch.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject; AccentColour.BindTo(ObjectState.AccentColour);
HyperDash.BindTo(ObjectState.HyperDash);
if (hitObject != null)
{
AccentColour.BindTo(hitObject.AccentColour);
HyperDash.BindTo(hitObject.HyperDash);
}
hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ?? hyperSprite.Colour = Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value ??
Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ?? Skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value ??

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation) public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
{ {
var droppedObjectContainer = new Container var droppedObjectContainer = new Container<CaughtObject>
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}; };

View File

@ -53,9 +53,15 @@ namespace osu.Game.Rulesets.Catch.UI
private CatcherTrailDisplay trails; private CatcherTrailDisplay trails;
private readonly Container droppedObjectTarget; /// <summary>
/// Contains caught objects on the plate.
/// </summary>
private readonly Container<CaughtObject> caughtObjectContainer;
private readonly Container<DrawablePalpableCatchHitObject> caughtFruitContainer; /// <summary>
/// Contains objects dropped from the plate.
/// </summary>
private readonly Container<CaughtObject> droppedObjectTarget;
public CatcherAnimationState CurrentState { get; private set; } public CatcherAnimationState CurrentState { get; private set; }
@ -108,7 +114,11 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<HitExplosion> hitExplosionPool; private readonly DrawablePool<HitExplosion> hitExplosionPool;
private readonly Container<HitExplosion> hitExplosionContainer; private readonly Container<HitExplosion> hitExplosionContainer;
public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null) private readonly DrawablePool<CaughtFruit> caughtFruitPool;
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] Container trailsTarget, [NotNull] Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty = null)
{ {
this.trailsTarget = trailsTarget; this.trailsTarget = trailsTarget;
this.droppedObjectTarget = droppedObjectTarget; this.droppedObjectTarget = droppedObjectTarget;
@ -124,7 +134,11 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
hitExplosionPool = new DrawablePool<HitExplosion>(10), hitExplosionPool = new DrawablePool<HitExplosion>(10),
caughtFruitContainer = new Container<DrawablePalpableCatchHitObject> caughtFruitPool = new DrawablePool<CaughtFruit>(50),
caughtBananaPool = new DrawablePool<CaughtBanana>(100),
// less capacity is needed compared to fruit because droplet is not stacked
caughtDropletPool = new DrawablePool<CaughtDroplet>(25),
caughtObjectContainer = new Container<CaughtObject>
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
@ -172,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// Creates proxied content to be displayed beneath hitobjects. /// Creates proxied content to be displayed beneath hitobjects.
/// </summary> /// </summary>
public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy(); public Drawable CreateProxiedContent() => caughtObjectContainer.CreateProxy();
/// <summary> /// <summary>
/// Calculates the scale of the catcher based off the provided beatmap difficulty. /// Calculates the scale of the catcher based off the provided beatmap difficulty.
@ -215,10 +229,19 @@ namespace osu.Game.Rulesets.Catch.UI
catchResult.CatcherAnimationState = CurrentState; catchResult.CatcherAnimationState = CurrentState;
catchResult.CatcherHyperDash = HyperDashing; catchResult.CatcherHyperDash = HyperDashing;
if (!(drawableObject.HitObject is PalpableCatchHitObject hitObject)) return; if (!(drawableObject is DrawablePalpableCatchHitObject palpableObject)) return;
var hitObject = palpableObject.HitObject;
if (result.IsHit) if (result.IsHit)
placeCaughtObject(hitObject); {
var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2);
placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value)
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
}
// droplet doesn't affect the catcher state // droplet doesn't affect the catcher state
if (hitObject is TinyDroplet) return; if (hitObject is TinyDroplet) return;
@ -256,8 +279,8 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState(); SetHyperDashState();
} }
caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
droppedObjectTarget.RemoveAll(d => (d as DrawableCatchHitObject)?.HitObject == drawableObject.HitObject); droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject);
hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject);
} }
@ -441,138 +464,121 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher(); updateCatcher();
} }
private void placeCaughtObject(PalpableCatchHitObject source) private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position)
{ {
var caughtObject = createCaughtObject(source); var caughtObject = getCaughtObject(drawableObject.HitObject);
if (caughtObject == null) return; if (caughtObject == null) return;
caughtObject.RelativePositionAxes = Axes.None; caughtObject.CopyStateFrom(drawableObject);
caughtObject.X = source.X - X;
caughtObject.IsOnPlate = true;
caughtObject.Anchor = Anchor.TopCentre; caughtObject.Anchor = Anchor.TopCentre;
caughtObject.Origin = Anchor.Centre; caughtObject.Position = position;
caughtObject.Scale *= 0.5f; caughtObject.Scale /= 2;
caughtObject.LifetimeStart = source.StartTime;
caughtObject.LifetimeEnd = double.MaxValue;
adjustPositionInStack(caughtObject); caughtObjectContainer.Add(caughtObject);
caughtFruitContainer.Add(caughtObject);
addLighting(caughtObject);
if (!caughtObject.StaysOnPlate) if (!caughtObject.StaysOnPlate)
removeFromPlate(caughtObject, DroppedObjectAnimation.Explode); removeFromPlate(caughtObject, DroppedObjectAnimation.Explode);
} }
private void adjustPositionInStack(DrawablePalpableCatchHitObject caughtObject) private Vector2 computePositionInStack(Vector2 position, float displayRadius)
{ {
const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2; const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2;
const float allowance = 10; const float allowance = 10;
float caughtObjectRadius = caughtObject.DisplayRadius; while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2)))
while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, caughtObject.Position) < (caughtObjectRadius + radius_div_2) / (allowance / 2)))
{ {
float diff = (caughtObjectRadius + radius_div_2) / allowance; float diff = (displayRadius + radius_div_2) / allowance;
caughtObject.X += (RNG.NextSingle() - 0.5f) * diff * 2; position.X += (RNG.NextSingle() - 0.5f) * diff * 2;
caughtObject.Y -= RNG.NextSingle() * diff; position.Y -= RNG.NextSingle() * diff;
} }
caughtObject.X = Math.Clamp(caughtObject.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
return position;
} }
private void addLighting(DrawablePalpableCatchHitObject caughtObject) private void addLighting(CatchHitObject hitObject, float x, Color4 colour)
{ {
if (!hitLighting.Value) return;
HitExplosion hitExplosion = hitExplosionPool.Get(); HitExplosion hitExplosion = hitExplosionPool.Get();
hitExplosion.HitObject = caughtObject.HitObject; hitExplosion.HitObject = hitObject;
hitExplosion.X = caughtObject.X; hitExplosion.X = x;
hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale); hitExplosion.Scale = new Vector2(hitObject.Scale);
hitExplosion.ObjectColour = caughtObject.AccentColour.Value; hitExplosion.ObjectColour = colour;
hitExplosionContainer.Add(hitExplosion); hitExplosionContainer.Add(hitExplosion);
} }
private DrawablePalpableCatchHitObject createCaughtObject(PalpableCatchHitObject source) private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{ {
switch (source) switch (source)
{ {
case Banana banana: case Fruit _:
return new DrawableBanana(banana); return caughtFruitPool.Get();
case Fruit fruit: case Banana _:
return new DrawableFruit(fruit); return caughtBananaPool.Get();
case TinyDroplet tiny: case Droplet _:
return new DrawableTinyDroplet(tiny); return caughtDropletPool.Get();
case Droplet droplet:
return new DrawableDroplet(droplet);
default: default:
return null; return null;
} }
} }
private CaughtObject getDroppedObject(CaughtObject caughtObject)
{
var droppedObject = getCaughtObject(caughtObject.HitObject);
droppedObject.CopyStateFrom(caughtObject);
droppedObject.Anchor = Anchor.TopLeft;
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget);
return droppedObject;
}
private void clearPlate(DroppedObjectAnimation animation) private void clearPlate(DroppedObjectAnimation animation)
{ {
var caughtObjects = caughtFruitContainer.Children.ToArray(); var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray();
caughtFruitContainer.Clear(false);
droppedObjectTarget.AddRange(caughtObjects); caughtObjectContainer.Clear(false);
foreach (var caughtObject in caughtObjects) droppedObjectTarget.AddRange(droppedObjects);
drop(caughtObject, animation);
foreach (var droppedObject in droppedObjects)
applyDropAnimation(droppedObject, animation);
} }
private void removeFromPlate(DrawablePalpableCatchHitObject caughtObject, DroppedObjectAnimation animation) private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{ {
if (!caughtFruitContainer.Remove(caughtObject)) var droppedObject = getDroppedObject(caughtObject);
throw new InvalidOperationException("Can only drop a caught object on the plate");
droppedObjectTarget.Add(caughtObject); caughtObjectContainer.Remove(caughtObject);
drop(caughtObject, animation); droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation);
} }
private void drop(DrawablePalpableCatchHitObject d, DroppedObjectAnimation animation) private void applyDropAnimation(Drawable d, DroppedObjectAnimation animation)
{ {
var originalX = d.X * Scale.X; switch (animation)
var startTime = Clock.CurrentTime;
d.Anchor = Anchor.TopLeft;
d.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(d.DrawPosition, droppedObjectTarget);
// we cannot just apply the transforms because DHO clears transforms when state is updated
d.ApplyCustomUpdateState += (o, state) => animate(o, animation, originalX, startTime);
if (d.IsLoaded)
animate(d, animation, originalX, startTime);
}
private void animate(Drawable d, DroppedObjectAnimation animation, float originalX, double startTime)
{
using (d.BeginAbsoluteSequence(startTime))
{ {
switch (animation) case DroppedObjectAnimation.Drop:
{ d.MoveToY(d.Y + 75, 750, Easing.InSine);
case DroppedObjectAnimation.Drop: d.FadeOut(750);
d.MoveToY(d.Y + 75, 750, Easing.InSine); break;
d.FadeOut(750);
break;
case DroppedObjectAnimation.Explode: case DroppedObjectAnimation.Explode:
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X;
d.MoveToX(d.X + originalX * 6, 1000); d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.FadeOut(750); d.MoveToX(d.X + originalX * 6, 1000);
break; d.FadeOut(750);
} break;
d.Expire();
} }
d.Expire();
} }
private enum DroppedObjectAnimation private enum DroppedObjectAnimation

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.UI
public readonly Catcher MovableCatcher; public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay; private readonly CatchComboDisplay comboDisplay;
public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) public CatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty difficulty = null)
{ {
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[] Children = new Drawable[]

View File

@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
public abstract class DrawableTaikoRulesetTestScene : OsuTestScene public abstract class DrawableTaikoRulesetTestScene : OsuTestScene
{ {
protected const int DEFAULT_PLAYFIELD_CONTAINER_HEIGHT = 768;
protected DrawableTaikoRuleset DrawableRuleset { get; private set; } protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
protected Container PlayfieldContainer { get; private set; } protected Container PlayfieldContainer { get; private set; }
@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
Add(PlayfieldContainer = new Container Add(PlayfieldContainer = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 768, Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
}); });
} }

View File

@ -13,12 +13,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
public readonly HitResult Type; public readonly HitResult Type;
public DrawableTestHit(Hit hit, HitResult type = HitResult.Great) public DrawableTestHit(Hit hit, HitResult type = HitResult.Great, bool kiai = false)
: base(hit) : base(hit)
{ {
Type = type; Type = type;
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new EffectControlPoint { KiaiMode = kiai });
HitObject.ApplyDefaults(controlPoints, new BeatmapDifficulty());
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -2,14 +2,16 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -97,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
break; break;
case 6: case 6:
PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, DEFAULT_PLAYFIELD_CONTAINER_HEIGHT), 500);
break; break;
} }
} }
@ -106,13 +108,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo();
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
Hit hit = new Hit(); Hit hit = new Hit();
hit.ApplyDefaults(cpi, new BeatmapDifficulty()); var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h); DrawableRuleset.Playfield.Add(h);
@ -123,25 +120,27 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
var cpi = new ControlPointInfo(); Hit hit = new Hit
cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); {
IsStrong = true,
Hit hit = new Hit(); Samples = createSamples(strong: true)
hit.ApplyDefaults(cpi, new BeatmapDifficulty()); };
var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
DrawableRuleset.Playfield.Add(h); DrawableRuleset.Playfield.Add(h);
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
} }
private void addMissJudgement() private void addMissJudgement()
{ {
DrawableTestHit h; DrawableTestHit h;
DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit { StartTime = DrawableRuleset.Playfield.Time.Current }, HitResult.Miss)
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); {
Alpha = 0
});
((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(h.HitObject, new TaikoJudgement()) { Type = HitResult.Miss });
} }
private void addBarLine(bool major, double delay = scroll_time) private void addBarLine(bool major, double delay = scroll_time)
@ -173,6 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong, IsStrong = strong,
Samples = createSamples(strong: strong),
Duration = duration, Duration = duration,
TickRate = 8, TickRate = 8,
}; };
@ -190,7 +190,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit h = new Hit Hit h = new Hit
{ {
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong IsStrong = strong,
Samples = createSamples(HitType.Centre, strong)
}; };
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -203,7 +204,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
Hit h = new Hit Hit h = new Hit
{ {
StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong IsStrong = strong,
Samples = createSamples(HitType.Rim, strong)
}; };
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -211,14 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
DrawableRuleset.Playfield.Add(new DrawableHit(h)); DrawableRuleset.Playfield.Add(new DrawableHit(h));
} }
private class TestStrongNestedHit : DrawableStrongNestedHit // TODO: can be removed if a better way of handling colour/strong type and samples is developed
private IList<HitSampleInfo> createSamples(HitType? hitType = null, bool strong = false)
{ {
public TestStrongNestedHit(DrawableHitObject mainObject) var samples = new List<HitSampleInfo>();
: base(new StrongHitObject { StartTime = mainObject.HitObject.StartTime }, mainObject)
{
}
public override bool OnPressed(TaikoAction action) => false; if (hitType == HitType.Rim)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if (strong)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
return samples;
} }
} }
} }

View File

@ -104,7 +104,7 @@ namespace osu.Game.Beatmaps
string cacheFilePath = storage.GetFullPath(cache_database_name); string cacheFilePath = storage.GetFullPath(cache_database_name);
string compressedCacheFilePath = $"{cacheFilePath}.bz2"; string compressedCacheFilePath = $"{cacheFilePath}.bz2";
cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2"); cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}");
cacheDownloadRequest.Failed += ex => cacheDownloadRequest.Failed += ex =>
{ {

View File

@ -95,6 +95,16 @@ namespace osu.Game.Screens.Play
localGameplayClock = new LocalGameplayClock(userOffsetClock); localGameplayClock = new LocalGameplayClock(userOffsetClock);
GameplayClock.IsPaused.BindTo(IsPaused); GameplayClock.IsPaused.BindTo(IsPaused);
IsPaused.BindValueChanged(onPauseChanged);
}
private void onPauseChanged(ValueChangedEvent<bool> isPaused)
{
if (isPaused.NewValue)
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop());
else
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
} }
private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset;
@ -154,13 +164,16 @@ namespace osu.Game.Screens.Play
public void Start() public void Start()
{ {
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time if (!adjustableClock.IsRunning)
// This accounts for the audio clock source potentially taking time to enter a completely stopped state {
Seek(GameplayClock.CurrentTime); // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
adjustableClock.Start(); // This accounts for the audio clock source potentially taking time to enter a completely stopped state
IsPaused.Value = false; Seek(GameplayClock.CurrentTime);
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); adjustableClock.Start();
}
IsPaused.Value = false;
} }
/// <summary> /// <summary>
@ -199,8 +212,6 @@ namespace osu.Game.Screens.Play
public void Stop() public void Stop()
{ {
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop());
IsPaused.Value = true; IsPaused.Value = true;
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -71,8 +72,9 @@ namespace osu.Game.Screens.Play
} }
private bool readyForPush => private bool readyForPush =>
!playerConsumed
// don't push unless the player is completely loaded // don't push unless the player is completely loaded
player?.LoadState == LoadState.Ready && player?.LoadState == LoadState.Ready
// don't push if the user is hovering one of the panes, unless they are idle. // don't push if the user is hovering one of the panes, unless they are idle.
&& (IsHovered || idleTracker.IsIdle.Value) && (IsHovered || idleTracker.IsIdle.Value)
// don't push if the user is dragging a slider or otherwise. // don't push if the user is dragging a slider or otherwise.
@ -84,6 +86,11 @@ namespace osu.Game.Screens.Play
private Player player; private Player player;
/// <summary>
/// Whether the curent player instance has been consumed via <see cref="consumePlayer"/>.
/// </summary>
private bool playerConsumed;
private LogoTrackingContainer content; private LogoTrackingContainer content;
private bool hideOverlays; private bool hideOverlays;
@ -179,7 +186,10 @@ namespace osu.Game.Screens.Play
contentIn(); contentIn();
MetadataInfo.Delay(750).FadeIn(500); MetadataInfo.Delay(750).FadeIn(500);
this.Delay(1800).Schedule(pushWhenLoaded);
// after an initial delay, start the debounced load check.
// this will continue to execute even after resuming back on restart.
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, 1800, 0));
showMuteWarningIfNeeded(); showMuteWarningIfNeeded();
} }
@ -188,17 +198,18 @@ namespace osu.Game.Screens.Play
{ {
base.OnResuming(last); base.OnResuming(last);
contentIn(); // prepare for a retry.
player = null;
playerConsumed = false;
cancelLoad();
this.Delay(400).Schedule(pushWhenLoaded); contentIn();
} }
public override void OnSuspending(IScreen next) public override void OnSuspending(IScreen next)
{ {
base.OnSuspending(next); base.OnSuspending(next);
cancelLoad();
BackgroundBrightnessReduction = false; BackgroundBrightnessReduction = false;
// we're moving to player, so a period of silence is upcoming. // we're moving to player, so a period of silence is upcoming.
@ -274,6 +285,14 @@ namespace osu.Game.Screens.Play
} }
} }
private Player consumePlayer()
{
Debug.Assert(!playerConsumed);
playerConsumed = true;
return player;
}
private void prepareNewPlayer() private void prepareNewPlayer()
{ {
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
@ -315,64 +334,62 @@ namespace osu.Game.Screens.Play
{ {
if (!this.IsCurrentScreen()) return; if (!this.IsCurrentScreen()) return;
try if (!readyForPush)
{ {
if (!readyForPush) // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce
// if we become unready for push during the delay.
cancelLoad();
return;
}
// if a push has already been scheduled, no further action is required.
// this value is reset via cancelLoad() to allow a second usage of the same PlayerLoader screen.
if (scheduledPushPlayer != null)
return;
scheduledPushPlayer = Scheduler.AddDelayed(() =>
{
// ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared).
var consumedPlayer = consumePlayer();
contentOut();
TransformSequence<PlayerLoader> pushSequence = this.Delay(250);
// only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load).
if (epilepsyWarning?.IsAlive == true)
{ {
// as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce const double epilepsy_display_length = 3000;
// if we become unready for push during the delay.
cancelLoad(); pushSequence
return; .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible)
.TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint)
.Delay(epilepsy_display_length)
.Schedule(() =>
{
epilepsyWarning.Hide();
epilepsyWarning.Expire();
})
.Delay(EpilepsyWarning.FADE_DURATION);
} }
if (scheduledPushPlayer != null) pushSequence.Schedule(() =>
return;
scheduledPushPlayer = Scheduler.AddDelayed(() =>
{ {
contentOut(); if (!this.IsCurrentScreen()) return;
TransformSequence<PlayerLoader> pushSequence = this.Delay(250); LoadTask = null;
// only show if the warning was created (i.e. the beatmap needs it) // By default, we want to load the player and never be returned to.
// and this is not a restart of the map (the warning expires after first load). // Note that this may change if the player we load requested a re-run.
if (epilepsyWarning?.IsAlive == true) ValidForResume = false;
{
const double epilepsy_display_length = 3000;
pushSequence if (consumedPlayer.LoadedBeatmapSuccessfully)
.Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) this.Push(consumedPlayer);
.TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) else
.Delay(epilepsy_display_length) this.Exit();
.Schedule(() => });
{ }, 500);
epilepsyWarning.Hide();
epilepsyWarning.Expire();
})
.Delay(EpilepsyWarning.FADE_DURATION);
}
pushSequence.Schedule(() =>
{
if (!this.IsCurrentScreen()) return;
LoadTask = null;
// By default, we want to load the player and never be returned to.
// Note that this may change if the player we load requested a re-run.
ValidForResume = false;
if (player.LoadedBeatmapSuccessfully)
this.Push(player);
else
this.Exit();
});
}, 500);
}
finally
{
Schedule(pushWhenLoaded);
}
} }
private void cancelLoad() private void cancelLoad()
@ -390,7 +407,7 @@ namespace osu.Game.Screens.Play
if (isDisposing) if (isDisposing)
{ {
// if the player never got pushed, we should explicitly dispose it. // if the player never got pushed, we should explicitly dispose it.
DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose()); DisposalTask = LoadTask?.ContinueWith(_ => player?.Dispose());
} }
} }

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1203.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1212.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="Sentry" Version="2.1.8" /> <PackageReference Include="Sentry" Version="2.1.8" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

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