osu/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs

351 lines
13 KiB
C#

// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public partial class TestSceneCatcher : OsuTestScene
{
[Resolved]
private OsuConfigManager config { get; set; }
private DroppedObjectContainer droppedObjectContainer;
private TestCatcher catcher;
[SetUp]
public void SetUp() => Schedule(() =>
{
var difficulty = new BeatmapDifficulty
{
CircleSize = 0,
};
droppedObjectContainer = new DroppedObjectContainer();
Child = new Container
{
Anchor = Anchor.Centre,
Children = new Drawable[]
{
droppedObjectContainer,
catcher = new TestCatcher(droppedObjectContainer, difficulty),
}
};
});
[Test]
public void TestCatcherHyperStateReverted()
{
JudgementResult result1 = null;
JudgementResult result2 = null;
AddStep("catch hyper fruit", () =>
{
result1 = attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } });
});
AddStep("catch normal fruit", () =>
{
result2 = attemptCatch(new Fruit());
});
AddStep("revert second result", () =>
{
catcher.OnRevertResult(result2);
});
checkHyperDash(true);
AddStep("revert first result", () =>
{
catcher.OnRevertResult(result1);
});
checkHyperDash(false);
}
[Test]
public void TestCatcherAnimationStateReverted()
{
JudgementResult result = null;
AddStep("catch kiai fruit", () =>
{
result = attemptCatch(new TestKiaiFruit());
});
checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () =>
{
catcher.OnRevertResult(result);
});
checkState(CatcherAnimationState.Idle);
}
[Test]
public void TestCatcherCatchWidth()
{
float halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
AddStep("move catcher to center", () => catcher.X = CatchPlayfield.CENTER_X);
float leftPlateBounds = CatchPlayfield.CENTER_X - halfWidth;
float rightPlateBounds = CatchPlayfield.CENTER_X + halfWidth;
AddStep("catch fruit", () =>
{
attemptCatch(new Fruit { X = leftPlateBounds + 1 });
attemptCatch(new Fruit { X = rightPlateBounds - 1 });
});
checkPlate(2);
AddStep("miss fruit", () =>
{
attemptCatch(new Fruit { X = leftPlateBounds - 1 });
attemptCatch(new Fruit { X = rightPlateBounds + 1 });
});
checkPlate(2);
}
[Test]
public void TestFruitClampedToCatchableRegion()
{
AddStep("catch fruit left", () => attemptCatch(new Fruit { X = -CatchPlayfield.WIDTH }));
checkPlate(1);
AddStep("move catcher to right", () => catcher.X = CatchPlayfield.WIDTH);
AddStep("catch fruit right", () => attemptCatch(new Fruit { X = CatchPlayfield.WIDTH * 2 }));
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 TestLastBananaShouldClearPlateOnMiss()
{
AddStep("catch fruit", () => attemptCatch(new Fruit()));
checkPlate(1);
AddStep("miss banana", () => attemptCatch(new Banana { X = 100 }));
checkPlate(1);
AddStep("miss last banana", () => attemptCatch(new Banana { LastInCombo = true, X = 100 }));
checkPlate(0);
}
[Test]
public void TestLastBananaShouldClearPlateOnCatch()
{
AddStep("catch fruit", () => attemptCatch(new Fruit()));
checkPlate(1);
AddStep("catch banana", () => attemptCatch(new Banana()));
checkPlate(2);
AddStep("catch last banana", () => attemptCatch(new Banana { LastInCombo = true }));
checkPlate(0);
}
[Test]
public void TestCatcherRandomStacking()
{
AddStep("catch more fruits", () => attemptCatch(() => new Fruit
{
X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(Vector2.One)
}, 50));
}
[Test]
public void TestCatcherStackingSameCaughtPosition()
{
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 < 0));
}
[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);
}
[Test]
public void TestHitLightingColour()
{
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour",
() => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
}
[Test]
public void TestHitLightingDisabled()
{
AddStep("disable hit lighting", () => config.SetValue(OsuSetting.HitLighting, false));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
}
[Test]
public void TestAllExplodedObjectsAtUniquePositions()
{
AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
AddStep("catch normal fruit", () => attemptCatch(new Fruit { IndexInBeatmap = 2, LastInCombo = true }));
AddAssert("two fruit at distinct x coordinates",
() => this.ChildrenOfType<CaughtFruit>().Select(f => f.DrawPosition.X).Distinct(),
() => Has.Exactly(2).Items);
}
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(Func<CatchHitObject> hitObject, int count)
{
for (int i = 0; i < count; i++)
attemptCatch(hitObject());
}
private JudgementResult attemptCatch(CatchHitObject hitObject)
{
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableObject = createDrawableObject(hitObject);
var result = createResult(hitObject);
applyResult(drawableObject, result);
return result;
}
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)
{
// Load DHO to set colour of hit explosion correctly
Add(drawableObject);
drawableObject.OnLoadComplete += _ =>
{
catcher.OnNewResult(drawableObject, result);
drawableObject.Expire();
};
}
private JudgementResult createResult(CatchHitObject hitObject)
{
return new CatchJudgementResult(hitObject, hitObject.Judgement)
{
Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss
};
}
private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject)
{
switch (hitObject)
{
case Banana banana:
return new DrawableBanana(banana);
case Droplet droplet:
return new DrawableDroplet(droplet);
case Fruit fruit:
return new DrawableFruit(fruit);
default:
throw new ArgumentOutOfRangeException(nameof(hitObject));
}
}
public partial class TestCatcher : Catcher
{
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty)
: base(droppedObjectTarget, difficulty)
{
}
}
public class TestKiaiFruit : Fruit
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
}
}
}
}