// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Catch.Replays { internal class CatchAutoGenerator : AutoGenerator { public const double RELEASE_DELAY = 20; public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap; public CatchAutoGenerator(IBeatmap beatmap) : base(beatmap) { Replay = new Replay(); } protected Replay Replay; private CatchReplayFrame currentFrame; public override Replay Generate() { // todo: add support for HT DT const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; float lastPosition = CatchPlayfield.CENTER_X; double lastTime = 0; void moveToNext(CatchHitObject h) { float positionChange = Math.Abs(lastPosition - h.X); double timeAvailable = h.StartTime - lastTime; // So we can either make it there without a dash or not. // If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too) // The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour. double speedRequired = positionChange == 0 ? 0 : positionChange / timeAvailable; bool dashRequired = speedRequired > movement_speed; bool impossibleJump = speedRequired > movement_speed * 2; // todo: get correct catcher size, based on difficulty CS. const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f; if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X) { // we are already in the correct range. lastTime = h.StartTime; addFrame(h.StartTime, lastPosition); return; } if (impossibleJump) { addFrame(h.StartTime, h.X); } else if (h.HyperDash) { addFrame(h.StartTime - timeAvailable, lastPosition); addFrame(h.StartTime, h.X); } else if (dashRequired) { // we do a movement in two parts - the dash part then the normal part... double timeAtNormalSpeed = positionChange / movement_speed; double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable; double timeAtDashSpeed = timeWeNeedToSave / 2; float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable); // dash movement addFrame(h.StartTime - timeAvailable + 1, lastPosition, true); addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition); addFrame(h.StartTime, h.X); } else { double timeBefore = positionChange / movement_speed; addFrame(h.StartTime - timeBefore, lastPosition); addFrame(h.StartTime, h.X); } lastTime = h.StartTime; lastPosition = h.X; } foreach (var obj in Beatmap.HitObjects) { switch (obj) { case Fruit _: moveToNext(obj); break; } foreach (var nestedObj in obj.NestedHitObjects.Cast()) { switch (nestedObj) { case Banana _: case TinyDroplet _: case Droplet _: case Fruit _: moveToNext(nestedObj); break; } } } return Replay; } private void addFrame(double time, float? position = null, bool dashing = false) { // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame. if (Replay.Frames.Count == 0) Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null)); var last = currentFrame; currentFrame = new CatchReplayFrame(time, position, dashing, last); Replay.Frames.Add(currentFrame); } } }