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

View File

@ -117,7 +117,7 @@ private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColou
AddStep("create hyper-dashing catcher", () =>
{
Child = setupSkinHierarchy(catcherArea = new CatcherArea
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -17,8 +17,6 @@ public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
public int RandomSeed => HitObject?.RandomSeed ?? 0;

View File

@ -30,6 +30,8 @@ public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject
/// </summary>
public virtual bool StaysOnPlate => true;
public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor;
protected readonly Container ScaleContainer;
protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h)

View File

@ -36,21 +36,20 @@ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
{
var explodingFruitContainer = new Container
var droppedObjectContainer = new Container
{
RelativeSizeAxes = Axes.Both,
};
CatcherArea = new CatcherArea(difficulty)
CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
{
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
};
InternalChildren = new[]
{
explodingFruitContainer,
droppedObjectContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer,
CatcherArea,

View File

@ -17,7 +17,6 @@
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -47,19 +46,15 @@ public class Catcher : SkinReloadableDrawable, IKeyBindingHandler<CatchAction>
/// </summary>
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]
private readonly Container trailsTarget;
private CatcherTrailDisplay trails;
private readonly Container droppedObjectTarget;
private readonly Container<DrawablePalpableCatchHitObject> caughtFruitContainer;
public CatcherAnimationState CurrentState { get; private set; }
/// <summary>
@ -92,9 +87,9 @@ protected set
/// </summary>
private readonly float catchWidth;
private CatcherSprite catcherIdle;
private CatcherSprite catcherKiai;
private CatcherSprite catcherFail;
private readonly CatcherSprite catcherIdle;
private readonly CatcherSprite catcherKiai;
private readonly CatcherSprite catcherFail;
private CatcherSprite currentCatcher;
@ -108,12 +103,13 @@ protected set
private float hyperDashTargetPosition;
private Bindable<bool> hitLighting;
private DrawablePool<HitExplosion> hitExplosionPool;
private Container<HitExplosion> hitExplosionContainer;
private readonly DrawablePool<HitExplosion> hitExplosionPool;
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.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre;
@ -122,17 +118,15 @@ public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty =
Scale = calculateScale(difficulty);
catchWidth = CalculateCatchWidth(Scale);
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
InternalChildren = new Drawable[]
{
hitExplosionPool = new DrawablePool<HitExplosion>(10),
caughtFruitContainer,
caughtFruitContainer = new Container<DrawablePalpableCatchHitObject>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
{
Anchor = Anchor.TopCentre,
@ -154,7 +148,12 @@ private void load(OsuConfigManager config)
Origin = Anchor.BottomCentre,
},
};
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
hitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
trails = new CatcherTrailDisplay(this);
updateCatcher();
@ -176,56 +175,19 @@ protected override void LoadComplete()
/// <summary>
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
/// </summary>
private static Vector2 calculateScale(BeatmapDifficulty difficulty)
=> new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
/// <summary>
/// Calculates the width of the area used for attempting catches in gameplay.
/// </summary>
/// <param name="scale">The scale of the catcher.</param>
internal static float CalculateCatchWidth(Vector2 scale)
=> CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
/// <summary>
/// Calculates the width of the area used for attempting catches in gameplay.
/// </summary>
/// <param name="difficulty">The beatmap difficulty.</param>
internal static float CalculateCatchWidth(BeatmapDifficulty 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);
}
}
internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
/// <summary>
/// Let the catcher attempt to catch a fruit.
@ -247,7 +209,10 @@ public bool AttemptCatch(CatchHitObject hitObject)
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 (validCatch && fruit.HyperDash)
@ -301,24 +266,17 @@ public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
}
}
private void runHyperDashStateTransition(bool hyperDashing)
public void UpdatePosition(float position)
{
updateTrailVisibility();
position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
if (hyperDashing)
{
this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
else
{
this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
if (position == X)
return;
Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
X = position;
}
private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
public bool OnPressed(CatchAction action)
{
switch (action)
@ -357,56 +315,34 @@ public void OnReleased(CatchAction action)
}
}
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>
/// Drop any fruit off the plate.
/// </summary>
public void Drop()
{
foreach (var f in caughtFruitContainer.ToArray())
Drop(f);
}
public void Drop() => clearPlate(DroppedObjectAnimation.Drop);
/// <summary>
/// Explode any fruit off the plate.
/// Explode all fruit off the plate.
/// </summary>
public void Explode()
{
foreach (var f in caughtFruitContainer.ToArray())
Explode(f);
}
public void Explode() => clearPlate(DroppedObjectAnimation.Explode);
public void Drop(DrawableHitObject fruit)
private void runHyperDashStateTransition(bool hyperDashing)
{
removeFromPlateWithTransform(fruit, f =>
updateTrailVisibility();
if (hyperDashing)
{
f.MoveToY(f.Y + 75, 750, Easing.InSine);
f.FadeOut(750);
});
}
public void Explode(DrawableHitObject fruit)
{
var originalX = fruit.X * Scale.X;
removeFromPlateWithTransform(fruit, f =>
this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
}
else
{
f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine);
f.MoveToX(f.X + originalX * 6, 1000);
f.FadeOut(750);
});
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;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
@ -479,33 +415,143 @@ private void updateState(CatcherAnimationState state)
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;
fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
float diff = (caughtObjectRadius + radius_div_2) / allowance;
if (!caughtFruitContainer.Remove(fruit))
// we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
return;
ExplodingFruitTarget.Add(fruit);
caughtObject.X += (RNG.NextSingle() - 0.5f) * diff * 2;
caughtObject.Y -= RNG.NextSingle() * diff;
}
var actionTime = Clock.CurrentTime;
caughtObject.X = Math.Clamp(caughtObject.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
}
fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState;
onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value);
private void addLighting(DrawablePalpableCatchHitObject caughtObject)
{
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))
action(fruit);
case Banana banana:
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
@ -23,14 +22,7 @@ public class CatcherArea : Container
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
public Container ExplodingFruitTarget
{
set => MovableCatcher.ExplodingFruitTarget = value;
}
private DrawableCatchHitObject lastPlateableFruit;
public CatcherArea(BeatmapDifficulty difficulty = null)
public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[]
@ -44,7 +36,7 @@ public CatcherArea(BeatmapDifficulty difficulty = null)
Margin = new MarginPadding { Bottom = 350f },
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 @@ public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result
if (!result.Type.IsScorable())
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 (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
runAfterLoaded(() => MovableCatcher.Explode());
MovableCatcher.Explode();
else
MovableCatcher.Drop();
}
@ -104,10 +59,6 @@ void runAfterLoaded(Action action)
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result);
public void OnReleased(CatchAction action)
{
}
public bool AttemptCatch(CatchHitObject obj)
{
return MovableCatcher.AttemptCatch(obj);
@ -124,26 +75,5 @@ protected override void UpdateAfterChildren()
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;
}
}
}
}