// 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 System.Collections.Generic; using System.Linq; using osuTK; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Framework.Extensions.IEnumerableExtensions; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { internal class HitObjectPatternGenerator : PatternGenerator { public PatternType StairType { get; private set; } private readonly PatternType convertType; public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { StairType = lastStair; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime); var positionData = hitObject as IHasPosition; float positionSeparation = ((positionData?.Position ?? Vector2.Zero) - previousPosition).Length; double timeSeparation = hitObject.StartTime - previousTime; if (timeSeparation <= 80) { // More than 187 BPM convertType |= PatternType.ForceNotStack | PatternType.KeepSingle; } else if (timeSeparation <= 95) { // More than 157 BPM convertType |= PatternType.ForceNotStack | PatternType.KeepSingle | lastStair; } else if (timeSeparation <= 105) { // More than 140 BPM convertType |= PatternType.ForceNotStack | PatternType.LowProbability; } else if (timeSeparation <= 125) { // More than 120 BPM convertType |= PatternType.ForceNotStack; } else if (timeSeparation <= 135 && positionSeparation < 20) { // More than 111 BPM stream convertType |= PatternType.Cycle | PatternType.KeepSingle; } else if (timeSeparation <= 150 && positionSeparation < 20) { // More than 100 BPM stream convertType |= PatternType.ForceStack | PatternType.LowProbability; } else if (positionSeparation < 20 && density >= timingPoint.BeatLength / 2.5) { // Low density stream convertType |= PatternType.Reverse | PatternType.LowProbability; } else if (density < timingPoint.BeatLength / 2.5 || effectPoint.KiaiMode) { // High density } else convertType |= PatternType.LowProbability; if (!convertType.HasFlag(PatternType.KeepSingle)) { if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8) convertType |= PatternType.Mirror; else if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) convertType |= PatternType.Gathered; } } public override IEnumerable<Pattern> Generate() { Pattern generateCore() { var pattern = new Pattern(); if (TotalColumns == 1) { addToPattern(pattern, 0); return pattern; } int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by copying the last hit objects in reverse-column order for (int i = RandomStart; i < TotalColumns; i++) { if (PreviousPattern.ColumnHasObject(i)) addToPattern(pattern, RandomStart + TotalColumns - i - 1); } return pattern; } if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 // If we convert to 7K + 1, let's not overload the special key && (TotalColumns != 8 || lastColumn != 0) // Make sure the last column was not the centre column && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) { // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) int column = RandomStart + TotalColumns - lastColumn - 1; addToPattern(pattern, column); return pattern; } if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by placing on the already filled columns for (int i = RandomStart; i < TotalColumns; i++) { if (PreviousPattern.ColumnHasObject(i)) addToPattern(pattern, i); } return pattern; } if (PreviousPattern.HitObjects.Count() == 1) { if (convertType.HasFlag(PatternType.Stair)) { // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" int targetColumn = lastColumn + 1; if (targetColumn == TotalColumns) targetColumn = RandomStart; addToPattern(pattern, targetColumn); return pattern; } if (convertType.HasFlag(PatternType.ReverseStair)) { // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" int targetColumn = lastColumn - 1; if (targetColumn == RandomStart - 1) targetColumn = TotalColumns - 1; addToPattern(pattern, targetColumn); return pattern; } } if (convertType.HasFlag(PatternType.KeepSingle)) return generateRandomNotes(1); if (convertType.HasFlag(PatternType.Mirror)) { if (ConversionDifficulty > 6.5) return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); if (ConversionDifficulty > 4) return generateRandomPatternWithMirrored(0.12, 0.17, 0); return generateRandomPatternWithMirrored(0.12, 0, 0); } if (ConversionDifficulty > 6.5) { if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(1, 0.62, 0, 0); } if (ConversionDifficulty > 4) { if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0); } if (ConversionDifficulty > 2) { if (convertType.HasFlag(PatternType.LowProbability)) return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0); } return generateRandomPattern(0, 0, 0, 0); } var p = generateCore(); foreach (var obj in p.HitObjects) { if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1) StairType = PatternType.ReverseStair; if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart) StairType = PatternType.Stair; } return p.Yield(); } /// <summary> /// Generates random notes. /// <para> /// This will generate as many as it can up to <paramref name="noteCount"/>, accounting for /// any stacks if <see cref="convertType"/> is forcing no stacks. /// </para> /// </summary> /// <param name="noteCount">The amount of notes to generate.</param> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> private Pattern generateRandomNotes(int noteCount) { var pattern = new Pattern(); bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack); if (!allowStacking) noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); for (int i = 0; i < noteCount; i++) { nextColumn = allowStacking ? FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: pattern) : FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: new[] { pattern, PreviousPattern }); addToPattern(pattern, nextColumn); } return pattern; int getNextColumn(int last) { if (convertType.HasFlag(PatternType.Gathered)) { last++; if (last == TotalColumns) last = RandomStart; } else last = GetRandomColumn(); return last; } } /// <summary> /// Whether this hit object can generate a note in the special column. /// </summary> private bool hasSpecialColumn => HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP) && HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); /// <summary> /// Generates a random pattern. /// </summary> /// <param name="p2">Probability for 2 notes to be generated.</param> /// <param name="p3">Probability for 3 notes to be generated.</param> /// <param name="p4">Probability for 4 notes to be generated.</param> /// <param name="p5">Probability for 5 notes to be generated.</param> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> private Pattern generateRandomPattern(double p2, double p3, double p4, double p5) { var pattern = new Pattern(); pattern.Add(generateRandomNotes(getRandomNoteCount(p2, p3, p4, p5))); if (RandomStart > 0 && hasSpecialColumn) addToPattern(pattern, 0); return pattern; } /// <summary> /// Generates a random pattern which has both normal and mirrored notes. /// </summary> /// <param name="centreProbability">The probability for a note to be added to the centre column.</param> /// <param name="p2">Probability for 2 notes to be generated.</param> /// <param name="p3">Probability for 3 notes to be generated.</param> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { if (convertType.HasFlag(PatternType.ForceNotStack)) return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); var pattern = new Pattern(); int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out var addToCentre); int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2; int nextColumn = GetRandomColumn(upperBound: columnLimit); for (int i = 0; i < noteCount; i++) { nextColumn = FindAvailableColumn(nextColumn, upperBound: columnLimit, patterns: pattern); // Add normal note addToPattern(pattern, nextColumn); // Add mirrored note addToPattern(pattern, RandomStart + TotalColumns - nextColumn - 1); } if (addToCentre) addToPattern(pattern, TotalColumns / 2); if (RandomStart > 0 && hasSpecialColumn) addToPattern(pattern, 0); return pattern; } /// <summary> /// Generates a count of notes to be generated from a list of probabilities. /// </summary> /// <param name="p2">Probability for 2 notes to be generated.</param> /// <param name="p3">Probability for 3 notes to be generated.</param> /// <param name="p4">Probability for 4 notes to be generated.</param> /// <param name="p5">Probability for 5 notes to be generated.</param> /// <returns>The amount of notes to be generated.</returns> private int getRandomNoteCount(double p2, double p3, double p4, double p5) { switch (TotalColumns) { case 2: p2 = 0; p3 = 0; p4 = 0; p5 = 0; break; case 3: p2 = Math.Min(p2, 0.1); p3 = 0; p4 = 0; p5 = 0; break; case 4: p2 = Math.Min(p2, 0.23); p3 = Math.Min(p3, 0.04); p4 = 0; p5 = 0; break; case 5: p3 = Math.Min(p3, 0.15); p4 = Math.Min(p4, 0.03); p5 = 0; break; } if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) p2 = 1; return GetRandomNoteCount(p2, p3, p4, p5); } /// <summary> /// Generates a count of notes to be generated from a list of probabilities. /// </summary> /// <param name="centreProbability">The probability for a note to be added to the centre column.</param> /// <param name="p2">Probability for 2 notes to be generated.</param> /// <param name="p3">Probability for 3 notes to be generated.</param> /// <param name="addToCentre">Whether to add a note to the centre column.</param> /// <returns>The amount of notes to be generated. The note to be added to the centre column will NOT be part of this count.</returns> private int getRandomNoteCountMirrored(double centreProbability, double p2, double p3, out bool addToCentre) { switch (TotalColumns) { case 2: centreProbability = 0; p2 = 0; p3 = 0; break; case 3: centreProbability = Math.Min(centreProbability, 0.03); p2 = 0; p3 = 0; break; case 4: centreProbability = 0; p2 = Math.Min(p2 * 2, 0.2); p3 = 0; break; case 5: centreProbability = Math.Min(centreProbability, 0.03); p3 = 0; break; case 6: centreProbability = 0; p2 = Math.Min(p2 * 2, 0.5); p3 = Math.Min(p3 * 2, 0.15); break; } double centreVal = Random.NextDouble(); int noteCount = GetRandomNoteCount(p2, p3); addToCentre = TotalColumns % 2 != 0 && noteCount != 3 && centreVal > 1 - centreProbability; return noteCount; } /// <summary> /// Constructs and adds a note to a pattern. /// </summary> /// <param name="pattern">The pattern to add to.</param> /// <param name="column">The column to add the note to.</param> private void addToPattern(Pattern pattern, int column) { pattern.Add(new Note { StartTime = HitObject.StartTime, Samples = HitObject.Samples, Column = column }); } } }