1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 17:32:54 +08:00

Merge pull request #11071 from ekrctb/caught-object-refactor

This commit is contained in:
Dean Herbert 2020-12-04 17:17:38 +09:00 committed by GitHub
commit ff64ba1b08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 402 additions and 284 deletions

View File

@ -1,26 +1,194 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
[TestFixture] [TestFixture]
public class TestSceneCatcher : CatchSkinnableTestScene public class TestSceneCatcher : OsuTestScene
{ {
[BackgroundDependencyLoader] [Resolved]
private void load() private OsuConfigManager config { get; set; }
private Container droppedObjectContainer;
private TestCatcher catcher;
[SetUp]
public void SetUp() => Schedule(() =>
{ {
SetContents(() => new Catcher(new Container()) var difficulty = new BeatmapDifficulty
{
CircleSize = 0,
};
var trailContainer = new Container();
droppedObjectContainer = new Container();
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
Child = new Container
{ {
RelativePositionAxes = Axes.None,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Children = new Drawable[]
{
trailContainer,
droppedObjectContainer,
catcher
}
};
});
[Test]
public void TestCatcherCatchWidth()
{
var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
AddStep("catch fruit", () =>
{
attemptCatch(new Fruit { X = -halfWidth + 1 });
attemptCatch(new Fruit { X = halfWidth - 1 });
}); });
checkPlate(2);
AddStep("miss fruit", () =>
{
attemptCatch(new Fruit { X = -halfWidth - 1 });
attemptCatch(new Fruit { X = halfWidth + 1 });
});
checkPlate(2);
}
[Test]
public void TestFruitChangesCatcherState()
{
AddStep("miss fruit", () => attemptCatch(new Fruit { X = 100 }));
checkState(CatcherAnimationState.Fail);
AddStep("catch fruit", () => attemptCatch(new Fruit()));
checkState(CatcherAnimationState.Idle);
AddStep("catch kiai fruit", () => attemptCatch(new TestKiaiFruit()));
checkState(CatcherAnimationState.Kiai);
}
[Test]
public void TestNormalFruitResetsHyperDashState()
{
AddStep("catch hyper fruit", () => attemptCatch(new Fruit
{
HyperDashTarget = new Fruit { X = 100 }
}));
checkHyperDash(true);
AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
checkHyperDash(false);
}
[Test]
public void TestTinyDropletMissPreservesCatcherState()
{
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
{
HyperDashTarget = new Fruit { X = 100 }
}));
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
// catcher state and hyper dash state is preserved
checkState(CatcherAnimationState.Kiai);
checkHyperDash(true);
}
[Test]
public void TestBananaMissPreservesCatcherState()
{
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
{
HyperDashTarget = new Fruit { X = 100 }
}));
AddStep("miss banana", () => attemptCatch(new Banana { X = 100 }));
// catcher state is preserved but hyper dash state is reset
checkState(CatcherAnimationState.Kiai);
checkHyperDash(false);
}
[Test]
public void TestCatcherStacking()
{
AddStep("catch fruit", () => attemptCatch(new Fruit()));
checkPlate(1);
AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
catcher.CaughtObjects.Any(obj => obj.Y < -20));
}
[Test]
public void TestCatcherExplosionAndDropping()
{
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1);
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
AddStep("explode", () => catcher.Explode());
AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
AddStep("catch fruits", () => attemptCatch(new Fruit(), 10));
AddStep("drop", () => catcher.Drop());
AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
}
[TestCase(true)]
[TestCase(false)]
public void TestHitLighting(bool enabled)
{
AddStep($"{(enabled ? "enable" : "disable")} hit lighting", () => config.Set(OsuSetting.HitLighting, enabled));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("check hit lighting", () => catcher.ChildrenOfType<HitExplosion>().Any() == enabled);
}
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
private void attemptCatch(CatchHitObject hitObject, int count = 1)
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
for (var i = 0; i < count; i++)
catcher.AttemptCatch(hitObject);
}
public class TestCatcher : Catcher
{
public IEnumerable<DrawablePalpableCatchHitObject> CaughtObjects => this.ChildrenOfType<DrawablePalpableCatchHitObject>();
public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, droppedObjectTarget, difficulty)
{
}
}
public class TestKiaiFruit : Fruit
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
}
} }
} }
} }

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -27,81 +28,68 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
private Catcher catcher => this.ChildrenOfType<CatcherArea>().First().MovableCatcher; private Catcher catcher => this.ChildrenOfType<Catcher>().First();
private float circleSize;
public TestSceneCatcherArea() public TestSceneCatcherArea()
{ {
AddSliderStep<float>("CircleSize", 0, 8, 5, createCatcher); AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
AddToggleStep("Hyperdash", t => AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
CreatedDrawables.OfType<CatchInputManager>().Select(i => i.Child)
.OfType<TestCatcherArea>().ForEach(c => c.ToggleHyperDash(t)));
AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false) AddStep("catch fruit", () => attemptCatch(new Fruit()));
{ AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true }));
X = catcher.X AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit()));
}), 20); AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true }));
AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
{
X = catcher.X,
LastInCombo = true,
}), 20);
AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
{
X = catcher.X
}), 20);
AddRepeatStep("miss fruit", () => catchFruit(new Fruit
{
X = catcher.X + 100,
LastInCombo = true,
}, true), 20);
} }
[TestCase(true)] private void attemptCatch(Fruit fruit)
[TestCase(false)]
public void TestHitLighting(bool enable)
{ {
AddStep("create catcher", () => createCatcher(5)); fruit.X += catcher.X;
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable));
AddStep("catch fruit", () => catchFruit(new TestFruit(false)
{ {
X = catcher.X CircleSize = circleSize
})); });
AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
{
X = catcher.X,
LastInCombo = true
}));
AddAssert("check hit explosion", () => catcher.ChildrenOfType<HitExplosion>().Any() == enable);
}
private void catchFruit(Fruit fruit, bool miss = false) foreach (var area in this.ChildrenOfType<CatcherArea>())
{
this.ChildrenOfType<CatcherArea>().ForEach(area =>
{ {
DrawableFruit drawable = new DrawableFruit(fruit); DrawableFruit drawable = new DrawableFruit(fruit);
area.Add(drawable); area.Add(drawable);
Schedule(() => Schedule(() =>
{ {
area.AttemptCatch(fruit); bool caught = area.AttemptCatch(fruit);
area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement())
{
Type = caught ? HitResult.Great : HitResult.Miss
});
drawable.Expire(); drawable.Expire();
}); });
}); }
} }
private void createCatcher(float size) private void createCatcher(float size)
{ {
SetContents(() => new CatchInputManager(catchRuleset) circleSize = size;
SetContents(() =>
{ {
RelativeSizeAxes = Axes.Both, var droppedObjectContainer = new Container();
Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
return new CatchInputManager(catchRuleset)
{ {
Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both,
Origin = Anchor.TopCentre, Children = new Drawable[]
}, {
droppedObjectContainer,
new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size })
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
}
}
};
}); });
} }
@ -111,26 +99,13 @@ namespace osu.Game.Rulesets.Catch.Tests
catchRuleset = rulesets.GetRuleset(2); catchRuleset = rulesets.GetRuleset(2);
} }
public class TestFruit : Fruit
{
public TestFruit(bool kiai)
{
var kiaiCpi = new ControlPointInfo();
kiaiCpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
ApplyDefaultsToSelf(kiaiCpi, new BeatmapDifficulty());
}
}
private class TestCatcherArea : CatcherArea private class TestCatcherArea : CatcherArea
{ {
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
: base(beatmapDifficulty) : base(droppedObjectContainer, beatmapDifficulty)
{ {
} }
public new Catcher MovableCatcher => base.MovableCatcher;
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1); public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
} }
} }

View File

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

View File

@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected override double InitialLifetimeOffset => HitObject.TimePreempt; protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
public int RandomSeed => HitObject?.RandomSeed ?? 0; public int RandomSeed => HitObject?.RandomSeed ?? 0;

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
/// </summary> /// </summary>
public virtual bool StaysOnPlate => true; public virtual bool StaysOnPlate => true;
public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor;
protected readonly Container ScaleContainer; protected readonly Container ScaleContainer;
protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)

View File

@ -36,21 +36,20 @@ 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 explodingFruitContainer = new Container var droppedObjectContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}; };
CatcherArea = new CatcherArea(difficulty) CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
{ {
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
}; };
InternalChildren = new[] InternalChildren = new[]
{ {
explodingFruitContainer, droppedObjectContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(), CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer, HitObjectContainer,
CatcherArea, CatcherArea,

View File

@ -17,7 +17,6 @@ using osu.Game.Configuration;
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.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -47,19 +46,15 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
public const double BASE_SPEED = 1.0; public const double BASE_SPEED = 1.0;
public Container ExplodingFruitTarget;
private Container<DrawableHitObject> caughtFruitContainer { get; } = new Container<DrawableHitObject>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
};
[NotNull] [NotNull]
private readonly Container trailsTarget; private readonly Container trailsTarget;
private CatcherTrailDisplay trails; private CatcherTrailDisplay trails;
private readonly Container droppedObjectTarget;
private readonly Container<DrawablePalpableCatchHitObject> caughtFruitContainer;
public CatcherAnimationState CurrentState { get; private set; } public CatcherAnimationState CurrentState { get; private set; }
/// <summary> /// <summary>
@ -92,9 +87,9 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private readonly float catchWidth; private readonly float catchWidth;
private CatcherSprite catcherIdle; private readonly CatcherSprite catcherIdle;
private CatcherSprite catcherKiai; private readonly CatcherSprite catcherKiai;
private CatcherSprite catcherFail; private readonly CatcherSprite catcherFail;
private CatcherSprite currentCatcher; private CatcherSprite currentCatcher;
@ -108,12 +103,13 @@ namespace osu.Game.Rulesets.Catch.UI
private float hyperDashTargetPosition; private float hyperDashTargetPosition;
private Bindable<bool> hitLighting; private Bindable<bool> hitLighting;
private DrawablePool<HitExplosion> hitExplosionPool; private readonly DrawablePool<HitExplosion> hitExplosionPool;
private Container<HitExplosion> hitExplosionContainer; private readonly Container<HitExplosion> hitExplosionContainer;
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null)
{ {
this.trailsTarget = trailsTarget; this.trailsTarget = trailsTarget;
this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
@ -122,17 +118,15 @@ namespace osu.Game.Rulesets.Catch.UI
Scale = calculateScale(difficulty); Scale = calculateScale(difficulty);
catchWidth = CalculateCatchWidth(Scale); catchWidth = CalculateCatchWidth(Scale);
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
hitExplosionPool = new DrawablePool<HitExplosion>(10), hitExplosionPool = new DrawablePool<HitExplosion>(10),
caughtFruitContainer, caughtFruitContainer = new Container<DrawablePalpableCatchHitObject>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -154,7 +148,12 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
}, },
}; };
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
trails = new CatcherTrailDisplay(this); trails = new CatcherTrailDisplay(this);
updateCatcher(); updateCatcher();
@ -176,56 +175,19 @@ namespace osu.Game.Rulesets.Catch.UI
/// <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.
/// </summary> /// </summary>
private static Vector2 calculateScale(BeatmapDifficulty difficulty) private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
=> new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
/// <summary> /// <summary>
/// Calculates the width of the area used for attempting catches in gameplay. /// Calculates the width of the area used for attempting catches in gameplay.
/// </summary> /// </summary>
/// <param name="scale">The scale of the catcher.</param> /// <param name="scale">The scale of the catcher.</param>
internal static float CalculateCatchWidth(Vector2 scale) internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
=> CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
/// <summary> /// <summary>
/// Calculates the width of the area used for attempting catches in gameplay. /// Calculates the width of the area used for attempting catches in gameplay.
/// </summary> /// </summary>
/// <param name="difficulty">The beatmap difficulty.</param> /// <param name="difficulty">The beatmap difficulty.</param>
internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
=> CalculateCatchWidth(calculateScale(difficulty));
/// <summary>
/// Add a caught fruit to the catcher's stack.
/// </summary>
/// <param name="fruit">The fruit that was caught.</param>
public void PlaceOnPlate(DrawableCatchHitObject fruit)
{
var ourRadius = fruit.DisplayRadius;
float theirRadius = 0;
const float allowance = 10;
while (caughtFruitContainer.Any(f =>
f.LifetimeEnd == double.MaxValue &&
Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
{
var diff = (ourRadius + theirRadius) / allowance;
fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2;
fruit.Y -= RNG.NextSingle() * diff;
}
fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
caughtFruitContainer.Add(fruit);
if (hitLighting.Value)
{
HitExplosion hitExplosion = hitExplosionPool.Get();
hitExplosion.X = fruit.X;
hitExplosion.Scale = new Vector2(fruit.HitObject.Scale);
hitExplosion.ObjectColour = fruit.AccentColour.Value;
hitExplosionContainer.Add(hitExplosion);
}
}
/// <summary> /// <summary>
/// Let the catcher attempt to catch a fruit. /// Let the catcher attempt to catch a fruit.
@ -247,7 +209,10 @@ namespace osu.Game.Rulesets.Catch.UI
catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth; catchObjectPosition <= catcherPosition + halfCatchWidth;
// only update hyperdash state if we are not catching a tiny droplet. if (validCatch)
placeCaughtObject(fruit);
// droplet doesn't affect the catcher state
if (fruit is TinyDroplet) return validCatch; if (fruit is TinyDroplet) return validCatch;
if (validCatch && fruit.HyperDash) if (validCatch && fruit.HyperDash)
@ -301,24 +266,17 @@ namespace osu.Game.Rulesets.Catch.UI
} }
} }
private void runHyperDashStateTransition(bool hyperDashing) public void UpdatePosition(float position)
{ {
updateTrailVisibility(); position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
if (hyperDashing) if (position == X)
{ return;
this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
} X = position;
else
{
this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
} }
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
public bool OnPressed(CatchAction action) public bool OnPressed(CatchAction action)
{ {
switch (action) switch (action)
@ -357,56 +315,34 @@ namespace osu.Game.Rulesets.Catch.UI
} }
} }
public void UpdatePosition(float position)
{
position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
if (position == X)
return;
Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
X = position;
}
/// <summary> /// <summary>
/// Drop any fruit off the plate. /// Drop any fruit off the plate.
/// </summary> /// </summary>
public void Drop() public void Drop() => clearPlate(DroppedObjectAnimation.Drop);
{
foreach (var f in caughtFruitContainer.ToArray())
Drop(f);
}
/// <summary> /// <summary>
/// Explode any fruit off the plate. /// Explode all fruit off the plate.
/// </summary> /// </summary>
public void Explode() public void Explode() => clearPlate(DroppedObjectAnimation.Explode);
{
foreach (var f in caughtFruitContainer.ToArray())
Explode(f);
}
public void Drop(DrawableHitObject fruit) private void runHyperDashStateTransition(bool hyperDashing)
{ {
removeFromPlateWithTransform(fruit, f => updateTrailVisibility();
if (hyperDashing)
{ {
f.MoveToY(f.Y + 75, 750, Easing.InSine); this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
f.FadeOut(750); this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}); }
} else
public void Explode(DrawableHitObject fruit)
{
var originalX = fruit.X * Scale.X;
removeFromPlateWithTransform(fruit, f =>
{ {
f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine); this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
f.MoveToX(f.X + originalX * 6, 1000); this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
f.FadeOut(750); }
});
} }
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin, allowFallback);
@ -479,33 +415,143 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher(); updateCatcher();
} }
private void removeFromPlateWithTransform(DrawableHitObject fruit, Action<DrawableHitObject> action) private void placeCaughtObject(PalpableCatchHitObject source)
{ {
if (ExplodingFruitTarget != null) var caughtObject = createCaughtObject(source);
if (caughtObject == null) return;
caughtObject.RelativePositionAxes = Axes.None;
caughtObject.X = source.X - X;
caughtObject.IsOnPlate = true;
caughtObject.Anchor = Anchor.TopCentre;
caughtObject.Origin = Anchor.Centre;
caughtObject.Scale *= 0.5f;
caughtObject.LifetimeStart = source.StartTime;
caughtObject.LifetimeEnd = double.MaxValue;
adjustPositionInStack(caughtObject);
caughtFruitContainer.Add(caughtObject);
addLighting(caughtObject);
if (!caughtObject.StaysOnPlate)
removeFromPlate(caughtObject, DroppedObjectAnimation.Explode);
}
private void adjustPositionInStack(DrawablePalpableCatchHitObject caughtObject)
{
const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2;
const float allowance = 10;
float caughtObjectRadius = caughtObject.DisplayRadius;
while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, caughtObject.Position) < (caughtObjectRadius + radius_div_2) / (allowance / 2)))
{ {
fruit.Anchor = Anchor.TopLeft; float diff = (caughtObjectRadius + radius_div_2) / allowance;
fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
if (!caughtFruitContainer.Remove(fruit)) caughtObject.X += (RNG.NextSingle() - 0.5f) * diff * 2;
// we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). caughtObject.Y -= RNG.NextSingle() * diff;
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
return;
ExplodingFruitTarget.Add(fruit);
} }
var actionTime = Clock.CurrentTime; caughtObject.X = Math.Clamp(caughtObject.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
}
fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; private void addLighting(DrawablePalpableCatchHitObject caughtObject)
onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); {
if (!hitLighting.Value) return;
void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) HitExplosion hitExplosion = hitExplosionPool.Get();
hitExplosion.X = caughtObject.X;
hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale);
hitExplosion.ObjectColour = caughtObject.AccentColour.Value;
hitExplosionContainer.Add(hitExplosion);
}
private DrawablePalpableCatchHitObject createCaughtObject(PalpableCatchHitObject source)
{
switch (source)
{ {
using (fruit.BeginAbsoluteSequence(actionTime)) case Banana banana:
action(fruit); return new DrawableBanana(banana);
fruit.Expire(); case Fruit fruit:
return new DrawableFruit(fruit);
case TinyDroplet tiny:
return new DrawableTinyDroplet(tiny);
case Droplet droplet:
return new DrawableDroplet(droplet);
default:
return null;
} }
} }
private void clearPlate(DroppedObjectAnimation animation)
{
var caughtObjects = caughtFruitContainer.Children.ToArray();
caughtFruitContainer.Clear(false);
droppedObjectTarget.AddRange(caughtObjects);
foreach (var caughtObject in caughtObjects)
drop(caughtObject, animation);
}
private void removeFromPlate(DrawablePalpableCatchHitObject caughtObject, DroppedObjectAnimation animation)
{
if (!caughtFruitContainer.Remove(caughtObject))
throw new InvalidOperationException("Can only drop a caught object on the plate");
droppedObjectTarget.Add(caughtObject);
drop(caughtObject, animation);
}
private void drop(DrawablePalpableCatchHitObject d, DroppedObjectAnimation animation)
{
var originalX = d.X * Scale.X;
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);
d.FadeOut(750);
break;
case DroppedObjectAnimation.Explode:
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);
break;
}
d.Expire();
}
}
private enum DroppedObjectAnimation
{
Drop,
Explode
}
} }
} }

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 System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -23,14 +22,7 @@ namespace osu.Game.Rulesets.Catch.UI
public readonly Catcher MovableCatcher; public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay; private readonly CatchComboDisplay comboDisplay;
public Container ExplodingFruitTarget public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
{
set => MovableCatcher.ExplodingFruitTarget = value;
}
private DrawableCatchHitObject lastPlateableFruit;
public CatcherArea(BeatmapDifficulty difficulty = null)
{ {
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[] Children = new Drawable[]
@ -44,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI
Margin = new MarginPadding { Bottom = 350f }, Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X X = CatchPlayfield.CENTER_X
}, },
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }, MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
}; };
} }
@ -53,47 +45,10 @@ namespace osu.Game.Rulesets.Catch.UI
if (!result.Type.IsScorable()) if (!result.Type.IsScorable())
return; return;
void runAfterLoaded(Action action)
{
if (lastPlateableFruit == null)
return;
// this is required to make this run after the last caught fruit runs updateState() at least once.
// TODO: find a better alternative
if (lastPlateableFruit.IsLoaded)
action();
else
lastPlateableFruit.OnLoadComplete += _ => action();
}
if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit)
{
// create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
var caughtFruit = createCaughtFruit(fruit);
if (caughtFruit == null) return;
caughtFruit.RelativePositionAxes = Axes.None;
caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(hitObject.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0);
caughtFruit.IsOnPlate = true;
caughtFruit.Anchor = Anchor.TopCentre;
caughtFruit.Origin = Anchor.Centre;
caughtFruit.Scale *= 0.5f;
caughtFruit.LifetimeStart = caughtFruit.HitObject.StartTime;
caughtFruit.LifetimeEnd = double.MaxValue;
MovableCatcher.PlaceOnPlate(caughtFruit);
lastPlateableFruit = caughtFruit;
if (!fruit.StaysOnPlate)
runAfterLoaded(() => MovableCatcher.Explode(caughtFruit));
}
if (hitObject.HitObject.LastInCombo) if (hitObject.HitObject.LastInCombo)
{ {
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
runAfterLoaded(() => MovableCatcher.Explode()); MovableCatcher.Explode();
else else
MovableCatcher.Drop(); MovableCatcher.Drop();
} }
@ -104,10 +59,6 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result); => comboDisplay.OnRevertResult(fruit, result);
public void OnReleased(CatchAction action)
{
}
public bool AttemptCatch(CatchHitObject obj) public bool AttemptCatch(CatchHitObject obj)
{ {
return MovableCatcher.AttemptCatch(obj); return MovableCatcher.AttemptCatch(obj);
@ -124,26 +75,5 @@ namespace osu.Game.Rulesets.Catch.UI
comboDisplay.X = MovableCatcher.X; comboDisplay.X = MovableCatcher.X;
} }
private DrawableCatchHitObject createCaughtFruit(DrawablePalpableCatchHitObject hitObject)
{
switch (hitObject.HitObject)
{
case Banana banana:
return new DrawableBanana(banana);
case Fruit fruit:
return new DrawableFruit(fruit);
case TinyDroplet tiny:
return new DrawableTinyDroplet(tiny);
case Droplet droplet:
return new DrawableDroplet(droplet);
default:
return null;
}
}
} }
} }