mirror of https://github.com/ppy/osu
Merge pull request #11071 from ekrctb/caught-object-refactor
This commit is contained in:
commit
ff64ba1b08
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue