// 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 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; 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 Generate() { yield return generate(); } private Pattern generate() { var pattern = new Pattern(); try { 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 pattern = generateRandomNotes(1); if (convertType.HasFlag(PatternType.Mirror)) { if (ConversionDifficulty > 6.5) return pattern = generateRandomPatternWithMirrored(0.12, 0.38, 0.12); if (ConversionDifficulty > 4) return pattern = generateRandomPatternWithMirrored(0.12, 0.17, 0); return pattern = generateRandomPatternWithMirrored(0.12, 0, 0); } if (ConversionDifficulty > 6.5) { if (convertType.HasFlag(PatternType.LowProbability)) return pattern = generateRandomPattern(0.78, 0.42, 0, 0); return pattern = generateRandomPattern(1, 0.62, 0, 0); } if (ConversionDifficulty > 4) { if (convertType.HasFlag(PatternType.LowProbability)) return pattern = generateRandomPattern(0.35, 0.08, 0, 0); return pattern = generateRandomPattern(0.52, 0.15, 0, 0); } if (ConversionDifficulty > 2) { if (convertType.HasFlag(PatternType.LowProbability)) return pattern = generateRandomPattern(0.18, 0, 0, 0); return pattern = generateRandomPattern(0.45, 0, 0, 0); } return pattern = generateRandomPattern(0, 0, 0, 0); } finally { foreach (var obj in pattern.HitObjects) { if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1) StairType = PatternType.ReverseStair; if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart) StairType = PatternType.Stair; } } } /// /// 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.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; } } /// /// Whether this hit object can generate a note in the special column. /// private bool hasSpecialColumn => HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP) && HitObject.Samples.Any(s => s.Name == HitSampleInfo.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) { if (convertType.HasFlag(PatternType.ForceNotStack)) return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); var pattern = new Pattern(); bool addToCentre; int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out 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; } /// /// 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 (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); } /// /// 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; 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; } /// /// 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 }); } } }