// 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.Collections.Generic; using System.Linq; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.Utils; namespace osu.Game.Rulesets.Osu.Mods { /// /// Mod that randomises the positions of the s /// public class OsuModRandom : ModRandom, IApplicableToBeatmap { public override string Description => "It never gets boring!"; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray(); private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; private Random? rng; public void ApplyToBeatmap(IBeatmap beatmap) { if (beatmap is not OsuBeatmap osuBeatmap) return; Seed.Value ??= RNG.Next(); rng = new Random((int)Seed.Value); var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects); // Offsets the angles of all hit objects in a "section" by the same amount. float sectionOffset = 0; // Whether the angles are positive or negative (clockwise or counter-clockwise flow). bool flowDirection = false; for (int i = 0; i < positionInfos.Count; i++) { if (shouldStartNewSection(osuBeatmap, positionInfos, i, 0.6f, 0.4f)) { sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(rng, 0, 0.0008f); flowDirection = !flowDirection; } 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 { // Offsets only the angle of the current hit object if a flow change occurs. float flowChangeOffset = 0; // Offsets only the angle of the current hit object. float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(rng, 0, 0.002f); if (shouldApplyFlowChange(positionInfos, i, 0.6f)) { flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(rng, 0, 0.002f); flowDirection = !flowDirection; } float totalOffset = // sectionOffset and oneTimeOffset should mainly affect patterns with large spacing. (sectionOffset + oneTimeOffset) * positionInfos[i].DistanceFromPrevious + // flowChangeOffset should mainly affect streams. flowChangeOffset * (playfield_diagonal - positionInfos[i].DistanceFromPrevious); positionInfos[i].RelativeAngle = getRelativeTargetAngle(positionInfos[i].DistanceFromPrevious, totalOffset, flowDirection); } } osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos); } /// The target distance between the previous and the current . /// The angle (in rad) by which the target angle should be offset. /// Whether the relative angle should be positive or negative. private static float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection) { float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310))) + 0.5 + offset); float relativeAngle = (float)Math.PI - angle; return flowDirection ? -relativeAngle : relativeAngle; } /// /// A new section should be started...
/// ...at the beginning of the .
/// ...on every combo start with a probability of (excluding new-combo-spam and 1-2-combos).
/// ...on every downbeat.
/// ...on every beat with a probability of .
///
/// Whether a new section should be started at the current . private bool shouldStartNewSection( OsuBeatmap beatmap, IReadOnlyList positionInfos, int i, float newComboProbability, float beatProbability ) => i == 0 || (positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 && positionInfos[i - 1].HitObject.NewCombo && rng?.NextDouble() < newComboProbability) || OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject, true) || (OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject) && rng?.NextDouble() < beatProbability); /// /// A flow change should occur on every combo start with a probability of (excluding new-combo-spam and 1-2-combos). /// /// Whether a flow change should be applied at the current . private bool shouldApplyFlowChange(IReadOnlyList positionInfos, int i, float probability) => positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 && positionInfos[i - 1].HitObject.NewCombo && rng?.NextDouble() < probability; } }