diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 4f83154728..475d5a2a67 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -37,23 +37,51 @@ namespace osu.Game.Rulesets.Osu.Mods var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects); - float rateOfChangeMultiplier = 0; + float sequenceOffset = 0; + bool flowDirection = false; - foreach (var positionInfo in positionInfos) + for (int i = 0; i < positionInfos.Count; i++) { - // rateOfChangeMultiplier only changes every 5 iterations in a combo - // to prevent shaky-line-shaped streams - if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0) - rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; + bool invertFlow = false; - if (positionInfo == positionInfos.First()) + if (i == 0 || + (positionInfos[i - 1].HitObject.NewCombo && (i <= 1 || !positionInfos[i - 2].HitObject.NewCombo) && (i <= 2 || !positionInfos[i - 3].HitObject.NewCombo)) || + OsuHitObjectGenerationUtils.IsHitObjectOnBeat(osuBeatmap, positionInfos[i - 1].HitObject, true) || + (OsuHitObjectGenerationUtils.IsHitObjectOnBeat(osuBeatmap, positionInfos[i - 1].HitObject) && rng.NextDouble() < 0.25)) { - positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2); - positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); + sequenceOffset = OsuHitObjectGenerationUtils.RandomGaussian(rng, 0, 0.02f); + + if (rng.NextDouble() < 0.6) + invertFlow = true; + } + + if (i == 0) + { + positionInfos[i].DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2); + positionInfos[i].RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else { - positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f)); + float flowChangeOffset = 0; + float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(rng, 0, 0.03f); + + if (positionInfos[i - 1].HitObject.NewCombo && (i <= 1 || !positionInfos[i - 2].HitObject.NewCombo) && rng.NextDouble() < 0.6) + { + flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(rng, 0, 0.05f); + + if (rng.NextDouble() < 0.8) + invertFlow = true; + } + + if (invertFlow) + flowDirection ^= true; + + positionInfos[i].RelativeAngle = OsuHitObjectGenerationUtils.GetRelativeTargetAngle( + positionInfos[i].DistanceFromPrevious, + (sequenceOffset + oneTimeOffset) * (float)Math.Sqrt(positionInfos[i].DistanceFromPrevious) + + flowChangeOffset * (float)Math.Sqrt(640 - positionInfos[i].DistanceFromPrevious), + flowDirection + ); } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 5e827d4782..9044fe142b 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -186,5 +187,37 @@ namespace osu.Game.Rulesets.Osu.Utils length * MathF.Sin(angle) ); } + + public static bool IsHitObjectOnBeat(OsuBeatmap beatmap, OsuHitObject hitObject, bool downbeatsOnly = false) + { + var timingPoints = beatmap.ControlPointInfo.TimingPoints; + var currentTimingPoint = timingPoints.Reverse().FirstOrDefault(p => p.Time <= hitObject.StartTime); + + if (currentTimingPoint == null) + return false; + + double timeSinceTimingPoint = hitObject.StartTime - currentTimingPoint.Time; + + double length = downbeatsOnly + ? currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator + : currentTimingPoint.BeatLength; + + return (timeSinceTimingPoint + 1) % length < 2; + } + + public static float GetRelativeTargetAngle(float targetDistance, float offset, bool flowDirection) + { + float angle = (float)(3.3 / (1 + 200 * Math.Pow(MathHelper.E, 0.016 * (targetDistance - 466))) + 0.45 + offset); + float relativeAngle = MathHelper.Pi - angle; + return flowDirection ? -relativeAngle : relativeAngle; + } + + public static float RandomGaussian(Random rng, float mean = 0, float stdDev = 1) + { + double x1 = 1 - rng.NextDouble(); + double x2 = 1 - rng.NextDouble(); + double stdNormal = Math.Sqrt(-2 * Math.Log(x1)) * Math.Sin(MathHelper.TwoPi * x2); + return mean + stdDev * (float)stdNormal; + } } }