diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 8446b7e70f..125d8cdded 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -12,11 +12,17 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Database; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; +using OpenTK; namespace osu.Game.Rulesets.Mania.Beatmaps { public class ManiaBeatmapConverter : BeatmapConverter { + /// + /// Maximum number of previous notes to consider for density calculation. + /// + private const int max_notes_for_density = 7; + protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; private Pattern lastPattern = new Pattern(); @@ -56,6 +62,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps yield return obj; } + private readonly List prevNoteTimes = new List(max_notes_for_density); + private double density = int.MaxValue; + private void computeDensity(double newNoteTime) + { + if (prevNoteTimes.Count == max_notes_for_density) + prevNoteTimes.RemoveAt(0); + prevNoteTimes.Add(newNoteTime); + + density = (prevNoteTimes[prevNoteTimes.Count - 1] - prevNoteTimes[0]) / prevNoteTimes.Count; + } + + private double lastTime; + private Vector2 lastPosition; + private PatternType lastStair; + private void recordNote(double time, Vector2 position) + { + lastTime = time; + lastPosition = position; + } + /// /// Method that generates hit objects for osu!mania specific beatmaps. /// @@ -92,7 +118,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps conversion = new EndTimeObjectPatternGenerator(random, original, beatmap); else if (positionData != null) { - // Circle + computeDensity(original.StartTime); + + conversion = new HitObjectPatternGenerator(random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair); + + recordNote(original.StartTime, positionData.Position); } if (conversion == null) @@ -101,6 +131,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Pattern newPattern = conversion.Generate(); lastPattern = newPattern; + var stairPatternGenerator = (HitObjectPatternGenerator)conversion; + lastStair = stairPatternGenerator.StairType; + return newPattern.HitObjects; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs new file mode 100644 index 0000000000..d044ee8893 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -0,0 +1,407 @@ +// 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.Timing; +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; + + ControlPoint overridePoint; + ControlPoint controlPoint = beatmap.TimingInfo.TimingPointAt(hitObject.StartTime, out overridePoint); + + var positionData = hitObject as IHasPosition; + + float positionSeparation = ((positionData?.Position ?? Vector2.Zero) - previousPosition).Length; + double timeSeparation = hitObject.StartTime - previousTime; + + double beatLength = controlPoint.BeatLength; + bool kiai = (overridePoint ?? controlPoint).KiaiMode; + + 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 >= beatLength / 2.5) + { + // Low density stream + convertType |= PatternType.Reverse | PatternType.LowProbability; + } + else if (density < beatLength / 2.5 || kiai) + { + // 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 + }); + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs b/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs index 2a0ce88506..674d83f6f2 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs @@ -140,6 +140,26 @@ namespace osu.Game.Rulesets.Mania.Judgements Miss = BeatmapDifficulty.DifficultyRange(difficulty, miss_max, miss_mid, miss_min); } + /// + /// Retrieves the hit result for a time offset. + /// + /// The time offset. + /// The hit result, or null if the time offset results in a miss. + public ManiaHitResult? ResultFor(double hitOffset) + { + if (hitOffset <= Perfect / 2) + return ManiaHitResult.Perfect; + if (hitOffset <= Great / 2) + return ManiaHitResult.Great; + if (hitOffset <= Good / 2) + return ManiaHitResult.Good; + if (hitOffset <= Ok / 2) + return ManiaHitResult.Ok; + if (hitOffset <= Bad / 2) + return ManiaHitResult.Bad; + return null; + } + /// /// Constructs new hit windows which have been multiplied by a value. /// diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaHitResult.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaHitResult.cs new file mode 100644 index 0000000000..207a1fb251 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaHitResult.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public enum ManiaHitResult + { + [Description("PERFECT")] + Perfect, + [Description("GREAT")] + Great, + [Description("GOOD")] + Good, + [Description("OK")] + Ok, + [Description("BAD")] + Bad + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 8dafbd01a5..6e69da3da7 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -10,5 +10,10 @@ namespace osu.Game.Rulesets.Mania.Judgements public override string ResultString => string.Empty; public override string MaxResultString => string.Empty; + + /// + /// The hit result. + /// + public ManiaHitResult ManiaResult; } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index d9e46f4720..f9d027e7ce 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -5,6 +5,8 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; +using osu.Framework.Configuration; +using OpenTK.Input; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -14,8 +16,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly BodyPiece bodyPiece; private readonly NotePiece tailPiece; - public DrawableHoldNote(HoldNote hitObject) - : base(hitObject) + public DrawableHoldNote(HoldNote hitObject, Bindable key = null) + : base(hitObject, key) { RelativeSizeAxes = Axes.Both; Height = (float)HitObject.Duration; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index d33a8c48ee..4e276fddb7 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -11,13 +13,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public abstract class DrawableManiaHitObject : DrawableHitObject where TObject : ManiaHitObject { + /// + /// The key that will trigger input for this hit object. + /// + protected Bindable Key { get; private set; } = new Bindable(); + public new TObject HitObject; - protected DrawableManiaHitObject(TObject hitObject) + protected DrawableManiaHitObject(TObject hitObject, Bindable key = null) : base(hitObject) { HitObject = hitObject; + if (key != null) + Key.BindTo(key); + RelativePositionAxes = Axes.Y; Y = (float)HitObject.StartTime; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index b216c362f5..d0519c61a8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Objects.Drawables; @@ -12,8 +14,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { private readonly NotePiece headPiece; - public DrawableNote(Note hitObject) - : base(hitObject) + public DrawableNote(Note hitObject, Bindable key = null) + : base(hitObject, key) { RelativeSizeAxes = Axes.Both; Height = 100; diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 701947c381..41bbe08d56 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The key-release hit windows for this hold note. /// - protected HitWindows ReleaseHitWindows = new HitWindows(); + public HitWindows ReleaseHitWindows { get; protected set; } = new HitWindows(); public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) { diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 1d2e4169b5..e955f6658b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The key-press hit window for this note. /// - protected HitWindows HitWindows = new HitWindows(); + public HitWindows HitWindows { get; protected set; } = new HitWindows(); public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) { diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 96f04f79d4..7a9572a0c7 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -22,5 +22,12 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override void OnNewJudgement(ManiaJudgement judgement) { } + + protected override void Reset() + { + base.Reset(); + + Health.Value = 1; + } } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index dea00433e6..72c60b28c9 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -18,6 +18,8 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Beatmaps.Timing; +using System; +using osu.Framework.Configuration; namespace osu.Game.Rulesets.Mania.UI { @@ -33,7 +35,10 @@ namespace osu.Game.Rulesets.Mania.UI private const float column_width = 45; private const float special_column_width = 70; - public Key Key; + /// + /// The key that will trigger input actions for this column and hit objects contained inside it. + /// + public Bindable Key = new Bindable(); private readonly Box background; private readonly Container hitTargetBar; @@ -95,6 +100,12 @@ namespace osu.Game.Rulesets.Mania.UI Name = "Hit objects", RelativeSizeAxes = Axes.Both, }, + // For column lighting, we need to capture input events before the notes + new InputTarget + { + KeyDown = onKeyDown, + KeyUp = onKeyUp + } } }, new Container @@ -178,12 +189,9 @@ namespace osu.Game.Rulesets.Mania.UI } } - public void Add(DrawableHitObject hitObject) - { - ControlPointContainer.Add(hitObject); - } + public void Add(DrawableHitObject hitObject) => ControlPointContainer.Add(hitObject); - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + private bool onKeyDown(InputState state, KeyDownEventArgs args) { if (args.Repeat) return false; @@ -197,7 +205,7 @@ namespace osu.Game.Rulesets.Mania.UI return false; } - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + private bool onKeyUp(InputState state, KeyUpEventArgs args) { if (args.Key == Key) { @@ -207,5 +215,24 @@ namespace osu.Game.Rulesets.Mania.UI return false; } + + /// + /// This is a simple container which delegates various input events that have to be captured before the notes. + /// + private class InputTarget : Container + { + public Func KeyDown; + public Func KeyUp; + + public InputTarget() + { + RelativeSizeAxes = Axes.Both; + AlwaysPresent = true; + Alpha = 0; + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => KeyDown?.Invoke(state, args) ?? false; + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => KeyUp?.Invoke(state, args) ?? false; + } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs index c67866dc10..4d734d231f 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using OpenTK; +using OpenTK.Input; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; @@ -76,13 +78,19 @@ namespace osu.Game.Rulesets.Mania.UI protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) { + var maniaPlayfield = Playfield as ManiaPlayfield; + if (maniaPlayfield == null) + return null; + + Bindable key = maniaPlayfield.Columns.ElementAt(h.Column).Key; + var holdNote = h as HoldNote; if (holdNote != null) - return new DrawableHoldNote(holdNote); + return new DrawableHoldNote(holdNote, key); var note = h as Note; if (note != null) - return new DrawableNote(note); + return new DrawableNote(note, key); return null; } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 56a86873e9..70bdd3b13c 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -55,7 +55,8 @@ namespace osu.Game.Rulesets.Mania.UI } } - public readonly FlowContainer Columns; + private readonly FlowContainer columns; + public IEnumerable Columns => columns.Children; private readonly ControlPointContainer barlineContainer; @@ -87,7 +88,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both, Colour = Color4.Black }, - Columns = new FillFlowContainer + columns = new FillFlowContainer { Name = "Columns", RelativeSizeAxes = Axes.Y, @@ -114,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.UI }; for (int i = 0; i < columnCount; i++) - Columns.Add(new Column(timingChanges)); + columns.Add(new Column(timingChanges)); TimeSpan = time_span_default; } @@ -133,17 +134,17 @@ namespace osu.Game.Rulesets.Mania.UI // Set the special column + colour + key for (int i = 0; i < columnCount; i++) { - Column column = Columns.Children.ElementAt(i); + Column column = Columns.ElementAt(i); column.IsSpecial = isSpecialColumn(i); if (!column.IsSpecial) continue; - column.Key = Key.Space; + column.Key.Value = Key.Space; column.AccentColour = specialColumnColour; } - var nonSpecialColumns = Columns.Children.Where(c => !c.IsSpecial).ToList(); + var nonSpecialColumns = Columns.Where(c => !c.IsSpecial).ToList(); // We'll set the colours of the non-special columns in a separate loop, because the non-special // column colours are mirrored across their centre and special styles mess with this @@ -162,11 +163,11 @@ namespace osu.Game.Rulesets.Mania.UI int keyOffset = default_keys.Length / 2 - nonSpecialColumns.Count / 2 + i; if (keyOffset >= 0 && keyOffset < default_keys.Length) - column.Key = default_keys[keyOffset]; + column.Key.Value = default_keys[keyOffset]; else // There is no default key defined for this column. Let's set this to Unknown for now // however note that this will be gone after bindings are in place - column.Key = Key.Unknown; + column.Key.Value = Key.Unknown; } } @@ -189,7 +190,7 @@ namespace osu.Game.Rulesets.Mania.UI } } - public override void Add(DrawableHitObject h) => Columns.Children.ElementAt(h.HitObject.Column).Add(h); + public override void Add(DrawableHitObject h) => Columns.ElementAt(h.HitObject.Column).Add(h); protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { @@ -225,7 +226,7 @@ namespace osu.Game.Rulesets.Mania.UI timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); barlineContainer.TimeSpan = value; - Columns.Children.ForEach(c => c.ControlPointContainer.TimeSpan = value); + Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value); } } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index adcdfd5fae..a3f30acae0 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -51,11 +51,13 @@ + + diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs new file mode 100644 index 0000000000..dfb742f7d1 --- /dev/null +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Graphics.Containers +{ + public class BeatSyncedContainer : Container + { + private readonly Bindable beatmap = new Bindable(); + + private int lastBeat; + private ControlPoint lastControlPoint; + + protected override void Update() + { + if (beatmap.Value?.Track == null) + return; + + double currentTrackTime = beatmap.Value.Track.CurrentTime; + ControlPoint overridePoint; + ControlPoint controlPoint = beatmap.Value.Beatmap.TimingInfo.TimingPointAt(currentTrackTime, out overridePoint); + + if (controlPoint.BeatLength == 0) + return; + + bool kiai = (overridePoint ?? controlPoint).KiaiMode; + int beat = (int)((currentTrackTime - controlPoint.Time) / controlPoint.BeatLength); + + // The beats before the start of the first control point are off by 1, this should do the trick + if (currentTrackTime < controlPoint.Time) + beat--; + + if (controlPoint == lastControlPoint && beat == lastBeat) + return; + + double offsetFromBeat = (controlPoint.Time - currentTrackTime) % controlPoint.BeatLength; + + using (BeginDelayedSequence(offsetFromBeat, true)) + OnNewBeat(beat, controlPoint.BeatLength, controlPoint.TimeSignature, kiai); + + lastBeat = beat; + lastControlPoint = controlPoint; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game) + { + beatmap.BindTo(game.Beatmap); + } + + protected virtual void OnNewBeat(int newBeat, double beatLength, TimeSignatures timeSignature, bool kiai) + { + } + } +} diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs new file mode 100644 index 0000000000..5fdb5e869e --- /dev/null +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -0,0 +1,171 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + /// + /// A container that can scroll to each section inside it. + /// + public class SectionsContainer : Container + { + private Drawable expandableHeader, fixedHeader, footer; + public readonly ScrollContainer ScrollContainer; + private readonly Container sectionsContainer; + + public Drawable ExpandableHeader + { + get { return expandableHeader; } + set + { + if (value == expandableHeader) return; + + if (expandableHeader != null) + Remove(expandableHeader); + expandableHeader = value; + if (value == null) return; + + Add(expandableHeader); + lastKnownScroll = float.NaN; + } + } + + public Drawable FixedHeader + { + get { return fixedHeader; } + set + { + if (value == fixedHeader) return; + + if (fixedHeader != null) + Remove(fixedHeader); + fixedHeader = value; + if (value == null) return; + + Add(fixedHeader); + lastKnownScroll = float.NaN; + } + } + + public Drawable Footer + { + get { return footer; } + set + { + if (value == footer) return; + + if (footer != null) + ScrollContainer.Remove(footer); + footer = value; + if (value == null) return; + + footer.Anchor |= Anchor.y2; + footer.Origin |= Anchor.y2; + ScrollContainer.Add(footer); + lastKnownScroll = float.NaN; + } + } + + public Bindable SelectedSection { get; } = new Bindable(); + + protected virtual Container CreateScrollContentContainer() + => new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both + }; + + private List sections = new List(); + public IEnumerable Sections + { + get { return sections; } + set + { + foreach (var section in sections) + sectionsContainer.Remove(section); + + sections = value.ToList(); + if (sections.Count == 0) return; + + sectionsContainer.Add(sections); + SelectedSection.Value = sections[0]; + lastKnownScroll = float.NaN; + } + } + + private float headerHeight, footerHeight; + private readonly MarginPadding originalSectionsMargin; + private void updateSectionsMargin() + { + if (sections.Count == 0) return; + + var newMargin = originalSectionsMargin; + newMargin.Top += headerHeight; + newMargin.Bottom += footerHeight; + + sectionsContainer.Margin = newMargin; + } + + public SectionsContainer() + { + Add(ScrollContainer = new ScrollContainer() + { + RelativeSizeAxes = Axes.Both, + Masking = false, + Children = new Drawable[] { sectionsContainer = CreateScrollContentContainer() } + }); + originalSectionsMargin = sectionsContainer.Margin; + } + + private float lastKnownScroll; + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + float headerH = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); + float footerH = Footer?.LayoutSize.Y ?? 0; + if (headerH != headerHeight || footerH != footerHeight) + { + headerHeight = headerH; + footerHeight = footerH; + updateSectionsMargin(); + } + + float currentScroll = Math.Max(0, ScrollContainer.Current); + if (currentScroll != lastKnownScroll) + { + lastKnownScroll = currentScroll; + + if (expandableHeader != null && fixedHeader != null) + { + float offset = Math.Min(expandableHeader.LayoutSize.Y, currentScroll); + + expandableHeader.Y = -offset; + fixedHeader.Y = -offset + expandableHeader.LayoutSize.Y; + } + + Drawable bestMatch = null; + float minDiff = float.MaxValue; + + foreach (var section in sections) + { + float diff = Math.Abs(ScrollContainer.GetChildPosInContent(section) - currentScroll); + if (diff < minDiff) + { + minDiff = diff; + bestMatch = section; + } + } + + if (bestMatch != null) + SelectedSection.Value = bestMatch; + } + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs index 56018dc7d9..c554b54a87 100644 --- a/osu.Game/Overlays/Settings/SettingsHeader.cs +++ b/osu.Game/Overlays/Settings/SettingsHeader.cs @@ -1,34 +1,16 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using OpenTK.Graphics; namespace osu.Game.Overlays.Settings { public class SettingsHeader : Container { - public SearchTextBox SearchTextBox; - - private Box background; - - private readonly Func currentScrollOffset; - - public Action Exit; - - /// A reference to the current scroll position of the ScrollContainer we are contained within. - public SettingsHeader(Func currentScrollOffset) - { - this.currentScrollOffset = currentScrollOffset; - } - [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -37,11 +19,6 @@ namespace osu.Game.Overlays.Settings Children = new Drawable[] { - background = new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -53,7 +30,8 @@ namespace osu.Game.Overlays.Settings { Text = "settings", TextSize = 40, - Margin = new MarginPadding { + Margin = new MarginPadding + { Left = SettingsOverlay.CONTENT_MARGINS, Top = Toolbar.Toolbar.TOOLTIP_HEIGHT }, @@ -63,45 +41,15 @@ namespace osu.Game.Overlays.Settings Colour = colours.Pink, Text = "Change the way osu! behaves", TextSize = 18, - Margin = new MarginPadding { + Margin = new MarginPadding + { Left = SettingsOverlay.CONTENT_MARGINS, Bottom = 30 }, }, - SearchTextBox = new SearchTextBox - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 0.95f, - Margin = new MarginPadding { - Top = 20, - Bottom = 20 - }, - Exit = () => Exit(), - }, } } }; } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - // the point at which we will start anchoring to the top. - float anchorOffset = SearchTextBox.Y; - - float scrollPosition = currentScrollOffset(); - - // we want to anchor the search field to the top of the screen when scrolling. - Margin = new MarginPadding { Top = Math.Max(0, scrollPosition - anchorOffset) }; - - // we don't want the header to scroll when scrolling beyond the upper extent. - Y = Math.Min(0, scrollPosition); - - // we get darker as scroll progresses - background.Alpha = Math.Min(1, scrollPosition / anchorOffset) * 0.5f; - } } } \ No newline at end of file diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index 766c6cf7e2..b6e4ad3f5e 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Settings private readonly Box backgroundBox; private readonly Box selectionIndicator; private readonly Container text; - public Action Action; + public Action Action; private SettingsSection section; public SettingsSection Section @@ -75,6 +75,7 @@ namespace osu.Game.Overlays.Settings { Width = Sidebar.DEFAULT_WIDTH, RelativeSizeAxes = Axes.Y, + Colour = OsuColour.Gray(0.6f), Children = new[] { headerText = new OsuSpriteText @@ -110,7 +111,7 @@ namespace osu.Game.Overlays.Settings protected override bool OnClick(InputState state) { - Action?.Invoke(); + Action?.Invoke(section); backgroundBox.FlashColour(Color4.White, 400); return true; } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 71ad18e081..943545e858 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -7,10 +7,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Overlays.Settings; -using System; -using osu.Game.Overlays.Settings.Sections; using osu.Framework.Input; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; namespace osu.Game.Overlays { @@ -26,18 +27,13 @@ namespace osu.Game.Overlays private const float sidebar_padding = 10; - private ScrollContainer scrollContainer; private Sidebar sidebar; private SidebarButton[] sidebarButtons; - private SettingsSection[] sections; + private SidebarButton selectedSidebarButton; - private SettingsHeader header; + private SettingsSectionsContainer sectionsContainer; - private SettingsFooter footer; - - private SearchContainer searchContainer; - - private float lastKnownScroll; + private SearchTextBox searchTextBox; public SettingsOverlay() { @@ -48,7 +44,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuGame game) { - sections = new SettingsSection[] + var sections = new SettingsSection[] { new GeneralSection(), new GraphicsSection(), @@ -68,27 +64,27 @@ namespace osu.Game.Overlays Colour = Color4.Black, Alpha = 0.6f, }, - scrollContainer = new ScrollContainer + sectionsContainer = new SettingsSectionsContainer { - ScrollDraggerVisible = false, RelativeSizeAxes = Axes.Y, Width = width, Margin = new MarginPadding { Left = SIDEBAR_WIDTH }, - Children = new Drawable[] + ExpandableHeader = new SettingsHeader(), + FixedHeader = searchTextBox = new SearchTextBox { - searchContainer = new SearchContainer + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Width = 0.95f, + Margin = new MarginPadding { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Children = sections, + Top = 20, + Bottom = 20 }, - footer = new SettingsFooter(), - header = new SettingsHeader(() => scrollContainer.Current) - { - Exit = Hide, - }, - } + Exit = Hide, + }, + Sections = sections, + Footer = new SettingsFooter() }, sidebar = new Sidebar { @@ -96,84 +92,89 @@ namespace osu.Game.Overlays Children = sidebarButtons = sections.Select(section => new SidebarButton { - Selected = sections[0] == section, Section = section, - Action = () => scrollContainer.ScrollIntoView(section), + Action = sectionsContainer.ScrollContainer.ScrollIntoView, } ).ToArray() } }; - header.SearchTextBox.Current.ValueChanged += newValue => searchContainer.SearchTerm = newValue; + selectedSidebarButton = sidebarButtons[0]; + selectedSidebarButton.Selected = true; - scrollContainer.Padding = new MarginPadding { Top = game?.Toolbar.DrawHeight ?? 0 }; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - //we need to update these manually because we can't put the SettingsHeader inside the SearchContainer (due to its anchoring). - searchContainer.Y = header.DrawHeight; - footer.Y = searchContainer.Y + searchContainer.DrawHeight; - } - - protected override void Update() - { - base.Update(); - - float currentScroll = scrollContainer.Current; - if (currentScroll != lastKnownScroll) + sectionsContainer.SelectedSection.ValueChanged += section => { - lastKnownScroll = currentScroll; + selectedSidebarButton.Selected = false; + selectedSidebarButton = sidebarButtons.Single(b => b.Section == section); + selectedSidebarButton.Selected = true; + }; - SettingsSection bestCandidate = null; - float bestDistance = float.MaxValue; + searchTextBox.Current.ValueChanged += newValue => sectionsContainer.SearchContainer.SearchTerm = newValue; - foreach (SettingsSection section in sections) - { - float distance = Math.Abs(scrollContainer.GetChildPosInContent(section) - currentScroll); - if (distance < bestDistance) - { - bestDistance = distance; - bestCandidate = section; - } - } - - var previous = sidebarButtons.SingleOrDefault(sb => sb.Selected); - var next = sidebarButtons.SingleOrDefault(sb => sb.Section == bestCandidate); - if (previous != null) previous.Selected = false; - if (next != null) next.Selected = true; - } + sectionsContainer.Padding = new MarginPadding { Top = game?.Toolbar.DrawHeight ?? 0 }; } protected override void PopIn() { base.PopIn(); - scrollContainer.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); + sectionsContainer.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); sidebar.MoveToX(0, TRANSITION_LENGTH, EasingTypes.OutQuint); FadeTo(1, TRANSITION_LENGTH / 2); - header.SearchTextBox.HoldFocus = true; + searchTextBox.HoldFocus = true; } protected override void PopOut() { base.PopOut(); - scrollContainer.MoveToX(-width, TRANSITION_LENGTH, EasingTypes.OutQuint); + sectionsContainer.MoveToX(-width, TRANSITION_LENGTH, EasingTypes.OutQuint); sidebar.MoveToX(-SIDEBAR_WIDTH, TRANSITION_LENGTH, EasingTypes.OutQuint); FadeTo(0, TRANSITION_LENGTH / 2); - header.SearchTextBox.HoldFocus = false; - header.SearchTextBox.TriggerFocusLost(); + searchTextBox.HoldFocus = false; + searchTextBox.TriggerFocusLost(); } protected override bool OnFocus(InputState state) { - header.SearchTextBox.TriggerFocus(state); + searchTextBox.TriggerFocus(state); return false; } + + private class SettingsSectionsContainer : SectionsContainer + { + public SearchContainer SearchContainer; + private readonly Box headerBackground; + + protected override Container CreateScrollContentContainer() + => SearchContainer = new SearchContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + }; + + public SettingsSectionsContainer() + { + ScrollContainer.ScrollDraggerVisible = false; + Add(headerBackground = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.X + }); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // no null check because the usage of this class is strict + headerBackground.Height = ExpandableHeader.LayoutSize.Y + FixedHeader.LayoutSize.Y; + headerBackground.Y = ExpandableHeader.Y; + headerBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f; + } + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1631311ef6..6946462e81 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -81,6 +81,7 @@ + @@ -292,6 +293,7 @@ +