// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using System.Linq; using OpenTK; 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; 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, Beatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair) : base(random, hitObject, beatmap, previousPattern) { 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 <= 125) { // More than 120 BPM convertType |= PatternType.ForceNotStack; } 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; } public override Pattern Generate() { int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by copying the last hit objects in reverse-column order var pattern = new Pattern(); for (int i = RandomStart; i < AvailableColumns; i++) if (PreviousPattern.ColumnHasObject(i)) addToPattern(pattern, RandomStart + AvailableColumns - i - 1); return pattern; } if ((convertType & PatternType.Cycle) > 0 && PreviousPattern.HitObjects.Count() == 1 // If we convert to 7K + 1, let's not overload the special key && (AvailableColumns != 8 || lastColumn != 0) // Make sure the last column was not the centre column && (AvailableColumns % 2 == 0 || lastColumn != AvailableColumns / 2)) { // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) var pattern = new Pattern(); int column = RandomStart + AvailableColumns - lastColumn - 1; addToPattern(pattern, column); return pattern; } if ((convertType & PatternType.ForceStack) > 0 && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by placing on the already filled columns var pattern = new Pattern(); for (int i = RandomStart; i < AvailableColumns; i++) if (PreviousPattern.ColumnHasObject(i)) addToPattern(pattern, i); return pattern; } if ((convertType & PatternType.Stair) > 0 && PreviousPattern.HitObjects.Count() == 1) { // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" var pattern = new Pattern(); int targetColumn = lastColumn + 1; if (targetColumn == AvailableColumns) { targetColumn = RandomStart; StairType = PatternType.ReverseStair; } addToPattern(pattern, targetColumn); return pattern; } if ((convertType & PatternType.ReverseStair) > 0 && PreviousPattern.HitObjects.Count() == 1) { // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" var pattern = new Pattern(); int targetColumn = lastColumn - 1; if (targetColumn == RandomStart - 1) { targetColumn = AvailableColumns - 1; StairType = PatternType.Stair; } addToPattern(pattern, targetColumn); return pattern; } if ((convertType & PatternType.KeepSingle) > 0) return generateRandomNotes(1); if ((convertType & PatternType.Mirror) > 0) { 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 & PatternType.LowProbability) > 0) return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(1, 0.62, 0, 0); } if (ConversionDifficulty > 4) { if ((convertType & PatternType.LowProbability) > 0) return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0); } if (ConversionDifficulty > 2) { if ((convertType & PatternType.LowProbability) > 0) return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0); } return generateRandomPattern(0, 0, 0, 0); } /// /// Generates random notes. /// /// This will generate as many as it can up to , accounting for /// any stacks if is forcing no stacks. /// /// /// The amount of notes to generate. /// The containing the hit objects. private Pattern generateRandomNotes(int noteCount) { var pattern = new Pattern(); bool allowStacking = (convertType & PatternType.ForceNotStack) == 0; if (!allowStacking) noteCount = Math.Min(noteCount, AvailableColumns - RandomStart - PreviousPattern.ColumnWithObjects); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); for (int i = 0; i < noteCount; i++) { while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking) { if ((convertType & PatternType.Gathered) > 0) { nextColumn++; if (nextColumn == AvailableColumns) nextColumn = RandomStart; } else nextColumn = Random.Next(RandomStart, AvailableColumns); } addToPattern(pattern, nextColumn); } return pattern; } /// /// Whether this hit object can generate a note in the special column. /// private bool hasSpecialColumn => HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_CLAP) && HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH); /// /// Generates a random pattern. /// /// Probability for 2 notes to be generated. /// Probability for 3 notes to be generated. /// Probability for 4 notes to be generated. /// Probability for 5 notes to be generated. /// The containing the hit objects. 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; } /// /// Generates a random pattern which has both normal and mirrored notes. /// /// The probability for a note to be added to the centre column. /// Probability for 2 notes to be generated. /// Probability for 3 notes to be generated. /// The containing the hit objects. private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { var pattern = new Pattern(); bool addToCentre; int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre); int columnLimit = (AvailableColumns % 2 == 0 ? AvailableColumns : AvailableColumns - 1) / 2; int nextColumn = Random.Next(RandomStart, columnLimit); for (int i = 0; i < noteCount; i++) { while (pattern.ColumnHasObject(nextColumn)) nextColumn = Random.Next(RandomStart, columnLimit); // Add normal note addToPattern(pattern, nextColumn); // Add mirrored note addToPattern(pattern, RandomStart + AvailableColumns - nextColumn - 1); } if (addToCentre) addToPattern(pattern, AvailableColumns / 2); if (RandomStart > 0 && hasSpecialColumn) addToPattern(pattern, 0); return pattern; } /// /// Generates a count of notes to be generated from a list of probabilities. /// /// Probability for 2 notes to be generated. /// Probability for 3 notes to be generated. /// Probability for 4 notes to be generated. /// Probability for 5 notes to be generated. /// The amount of notes to be generated. private int getRandomNoteCount(double p2, double p3, double p4, double p5) { switch (AvailableColumns) { case 2: p2 = 0; p3 = 0; p4 = 0; p5 = 0; break; case 3: p2 = Math.Max(p2, 0.1); p3 = 0; p4 = 0; p5 = 0; break; case 4: p2 = Math.Max(p2, 0.23); p3 = Math.Max(p3, 0.04); p4 = 0; p5 = 0; break; case 5: p3 = Math.Max(p3, 0.15); p4 = Math.Max(p4, 0.03); p5 = 0; break; } if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_CLAP)) p2 = 1; return GetRandomNoteCount(p2, p3, p4, p5); } /// /// Generates a count of notes to be generated from a list of probabilities. /// /// The probability for a note to be added to the centre column. /// Probability for 2 notes to be generated. /// Probability for 3 notes to be generated. /// Whether to add a note to the centre column. /// The amount of notes to be generated. The note to be added to the centre column will NOT be part of this count. private int getRandomNoteCountMirrored(double centreProbability, double p2, double p3, out bool addToCentre) { addToCentre = false; if ((convertType & PatternType.ForceNotStack) > 0) return getRandomNoteCount(p2 / 2, p2, (p2 + p3) / 2, p3); switch (AvailableColumns) { case 2: centreProbability = 0; p2 = 0; p3 = 0; break; case 3: centreProbability = Math.Max(centreProbability, 0.03); p2 = Math.Max(p2, 0.1); p3 = 0; break; case 4: centreProbability = 0; p2 = Math.Max(p2 * 2, 0.2); p3 = 0; break; case 5: centreProbability = Math.Max(centreProbability, 0.03); p3 = 0; break; case 6: centreProbability = 0; p2 = Math.Max(p2 * 2, 0.5); p3 = Math.Max(p3 * 2, 0.15); break; } double centreVal = Random.NextDouble(); int noteCount = GetRandomNoteCount(p2, p3); addToCentre = AvailableColumns % 2 != 0 && noteCount != 3 && centreVal > 1 - centreProbability; return noteCount; } /// /// Constructs and adds a note to a pattern. /// /// The pattern to add to. /// The column to add the note to. private void addToPattern(Pattern pattern, int column) { pattern.Add(new Note { StartTime = HitObject.StartTime, Samples = HitObject.Samples, Column = column }); } } }