diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 125d8cdded..0ed2a0ba6f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Database; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; using OpenTK; +using osu.Game.Audio; namespace osu.Game.Rulesets.Mania.Beatmaps { @@ -161,9 +162,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps pattern.Add(new HoldNote { StartTime = HitObject.StartTime, - Samples = HitObject.Samples, Duration = endTimeData.Duration, Column = column, + Head = { Samples = sampleInfoListAt(HitObject.StartTime) }, + Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) }, }); } else if (positionData != null) @@ -178,6 +180,24 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return pattern; } + + /// + /// Retrieves the sample info list at a point in time. + /// + /// The time to retrieve the sample info list from. + /// + private SampleInfoList sampleInfoListAt(double time) + { + var curveData = HitObject as IHasCurve; + + if (curveData == null) + return HitObject.Samples; + + double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.RepeatCount; + + int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); + return curveData.RepeatSamples[index]; + } } } } diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs index df2f7e9e63..d5cf57a5da 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs @@ -9,5 +9,29 @@ namespace osu.Game.Rulesets.Mania.Judgements /// Whether the hold note has been released too early and shouldn't give full score for the release. /// public bool HasBroken; + + public override int NumericResultForScore(ManiaHitResult result) + { + switch (result) + { + default: + return base.NumericResultForScore(result); + case ManiaHitResult.Great: + case ManiaHitResult.Perfect: + return base.NumericResultForScore(HasBroken ? ManiaHitResult.Good : result); + } + } + + public override int NumericResultForAccuracy(ManiaHitResult result) + { + switch (result) + { + default: + return base.NumericResultForAccuracy(result); + case ManiaHitResult.Great: + case ManiaHitResult.Perfect: + return base.NumericResultForAccuracy(HasBroken ? ManiaHitResult.Good : result); + } + } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs index bead455c13..852f97b3f2 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -5,5 +5,9 @@ namespace osu.Game.Rulesets.Mania.Judgements { public class HoldNoteTickJudgement : ManiaJudgement { + public override bool AffectsCombo => false; + + public override int NumericResultForScore(ManiaHitResult result) => 20; + public override int NumericResultForAccuracy(ManiaHitResult result) => 0; // Don't count ticks into accuracy } } \ 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 6e69da3da7..33083ca0f5 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -2,11 +2,37 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Judgements { public class ManiaJudgement : Judgement { + /// + /// The maximum possible hit result. + /// + public const ManiaHitResult MAX_HIT_RESULT = ManiaHitResult.Perfect; + + /// + /// The result value for the combo portion of the score. + /// + public int ResultValueForScore => Result == HitResult.Miss ? 0 : NumericResultForScore(ManiaResult); + + /// + /// The result value for the accuracy portion of the score. + /// + public int ResultValueForAccuracy => Result == HitResult.Miss ? 0 : NumericResultForAccuracy(ManiaResult); + + /// + /// The maximum result value for the combo portion of the score. + /// + public int MaxResultValueForScore => NumericResultForScore(MAX_HIT_RESULT); + + /// + /// The maximum result value for the accuracy portion of the score. + /// + public int MaxResultValueForAccuracy => NumericResultForAccuracy(MAX_HIT_RESULT); + public override string ResultString => string.Empty; public override string MaxResultString => string.Empty; @@ -15,5 +41,42 @@ namespace osu.Game.Rulesets.Mania.Judgements /// The hit result. /// public ManiaHitResult ManiaResult; + + public virtual int NumericResultForScore(ManiaHitResult result) + { + switch (result) + { + default: + return 0; + case ManiaHitResult.Bad: + return 50; + case ManiaHitResult.Ok: + return 100; + case ManiaHitResult.Good: + return 200; + case ManiaHitResult.Great: + case ManiaHitResult.Perfect: + return 300; + } + } + + public virtual int NumericResultForAccuracy(ManiaHitResult result) + { + switch (result) + { + default: + return 0; + case ManiaHitResult.Bad: + return 50; + case ManiaHitResult.Ok: + return 100; + case ManiaHitResult.Good: + return 200; + case ManiaHitResult.Great: + return 300; + case ManiaHitResult.Perfect: + return 305; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs new file mode 100644 index 0000000000..76a3d3920d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/BarLine.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 osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class BarLine : ManiaHitObject + { + /// + /// The control point which this bar line is part of. + /// + public TimingControlPoint ControlPoint; + + /// + /// The index of the beat which this bar line represents within the control point. + /// This is a "major" bar line if % == 0. + /// + public int BeatIndex; + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs new file mode 100644 index 0000000000..0b4d8b2d4e --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + /// + /// Visualises a . Although this derives DrawableManiaHitObject, + /// this does not handle input/sound like a normal hit object. + /// + public class DrawableBarLine : DrawableManiaHitObject + { + /// + /// Height of major bar line triangles. + /// + private const float triangle_height = 12; + + /// + /// Offset of the major bar line triangles from the sides of the bar line. + /// + private const float triangle_offset = 9; + + public DrawableBarLine(BarLine barLine) + : base(barLine) + { + RelativeSizeAxes = Axes.X; + Height = 1; + + Add(new Box + { + Name = "Bar line", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + }); + + bool isMajor = barLine.BeatIndex % (int)barLine.ControlPoint.TimeSignature == 0; + + if (isMajor) + { + Add(new EquilateralTriangle + { + Name = "Left triangle", + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopCentre, + Size = new Vector2(triangle_height), + X = -triangle_offset, + Rotation = 90 + }); + + Add(new EquilateralTriangle + { + Name = "Right triangle", + Anchor = Anchor.BottomRight, + Origin = Anchor.TopCentre, + Size = new Vector2(triangle_height), + X = triangle_offset, + Rotation = -90 + }); + } + + if (!isMajor && barLine.BeatIndex % 2 == 1) + Alpha = 0.2f; + } + + protected override void UpdateState(ArmedState state) + { + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index c241c4cf41..fa32d46a88 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . +// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; @@ -58,6 +58,9 @@ namespace osu.Game.Rulesets.Mania.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; + + Head.ApplyDefaults(controlPointInfo, difficulty); + Tail.ApplyDefaults(controlPointInfo, difficulty); } /// diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 7a9572a0c7..798d4b8c5b 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -1,8 +1,13 @@ // 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 osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -10,6 +15,143 @@ namespace osu.Game.Rulesets.Mania.Scoring { internal class ManiaScoreProcessor : ScoreProcessor { + /// + /// The maximum score achievable. + /// Does _not_ include bonus score - for bonus score see . + /// + private const int max_score = 1000000; + + /// + /// The amount of the score attributed to combo. + /// + private const double combo_portion_max = max_score * 0.2; + + /// + /// The amount of the score attributed to accuracy. + /// + private const double accuracy_portion_max = max_score * 0.8; + + /// + /// The factor used to determine relevance of combos. + /// + private const double combo_base = 4; + + /// + /// The combo value at which hit objects result in the max score possible. + /// + private const int combo_relevance_cap = 400; + + /// + /// The hit HP multiplier at OD = 0. + /// + private const double hp_multiplier_min = 0.75; + + /// + /// The hit HP multiplier at OD = 0. + /// + private const double hp_multiplier_mid = 0.85; + + /// + /// The hit HP multiplier at OD = 0. + /// + private const double hp_multiplier_max = 1; + + /// + /// The default BAD hit HP increase. + /// + private const double hp_increase_bad = 0.005; + + /// + /// The default OK hit HP increase. + /// + private const double hp_increase_ok = 0.010; + + /// + /// The default GOOD hit HP increase. + /// + private const double hp_increase_good = 0.035; + + /// + /// The default tick hit HP increase. + /// + private const double hp_increase_tick = 0.040; + + /// + /// The default GREAT hit HP increase. + /// + private const double hp_increase_great = 0.055; + + /// + /// The default PERFECT hit HP increase. + /// + private const double hp_increase_perfect = 0.065; + + /// + /// The MISS HP multiplier at OD = 0. + /// + private const double hp_multiplier_miss_min = 0.5; + + /// + /// The MISS HP multiplier at OD = 5. + /// + private const double hp_multiplier_miss_mid = 0.75; + + /// + /// The MISS HP multiplier at OD = 10. + /// + private const double hp_multiplier_miss_max = 1; + + /// + /// The default MISS HP increase. + /// + private const double hp_increase_miss = -0.125; + + /// + /// The MISS HP multiplier. This is multiplied to the miss hp increase. + /// + private double hpMissMultiplier = 1; + + /// + /// The HIT HP multiplier. This is multiplied to hit hp increases. + /// + private double hpMultiplier = 1; + + /// + /// The cumulative combo portion of the score. + /// + private double comboScore => combo_portion_max * comboPortion / maxComboPortion; + + /// + /// The cumulative accuracy portion of the score. + /// + private double accuracyScore => accuracy_portion_max * Math.Pow(Accuracy, 4) * totalHits / maxTotalHits; + + /// + /// The cumulative bonus score. + /// This is added on top of , thus the total score can exceed . + /// + private double bonusScore; + + /// + /// The achieved by a perfect playthrough. + /// + private double maxComboPortion; + + /// + /// The portion of the score dedicated to combo. + /// + private double comboPortion; + + /// + /// The achieved by a perfect playthrough. + /// + private int maxTotalHits; + + /// + /// The total hits. + /// + private int totalHits; + public ManiaScoreProcessor() { } @@ -19,8 +161,124 @@ namespace osu.Game.Rulesets.Mania.Scoring { } + protected override void ComputeTargets(Beatmap beatmap) + { + BeatmapDifficulty difficulty = beatmap.BeatmapInfo.Difficulty; + hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max); + hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max); + + while (true) + { + foreach (var obj in beatmap.HitObjects) + { + var holdNote = obj as HoldNote; + + if (obj is Note) + { + AddJudgement(new ManiaJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaHitResult.Perfect + }); + } + else if (holdNote != null) + { + // Head + AddJudgement(new ManiaJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaJudgement.MAX_HIT_RESULT + }); + + // Ticks + int tickCount = holdNote.Ticks.Count(); + for (int i = 0; i < tickCount; i++) + { + AddJudgement(new HoldNoteTickJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaJudgement.MAX_HIT_RESULT, + }); + } + + AddJudgement(new HoldNoteTailJudgement + { + Result = HitResult.Hit, + ManiaResult = ManiaJudgement.MAX_HIT_RESULT + }); + } + } + + if (!HasFailed) + break; + + hpMultiplier *= 1.01; + hpMissMultiplier *= 0.98; + + Reset(); + } + + maxTotalHits = totalHits; + maxComboPortion = comboPortion; + } + protected override void OnNewJudgement(ManiaJudgement judgement) { + bool isTick = judgement is HoldNoteTickJudgement; + + if (!isTick) + totalHits++; + + switch (judgement.Result) + { + case HitResult.Miss: + Health.Value += hpMissMultiplier * hp_increase_miss; + break; + case HitResult.Hit: + if (isTick) + { + Health.Value += hpMultiplier * hp_increase_tick; + bonusScore += judgement.ResultValueForScore; + } + else + { + switch (judgement.ManiaResult) + { + case ManiaHitResult.Bad: + Health.Value += hpMultiplier * hp_increase_bad; + break; + case ManiaHitResult.Ok: + Health.Value += hpMultiplier * hp_increase_ok; + break; + case ManiaHitResult.Good: + Health.Value += hpMultiplier * hp_increase_good; + break; + case ManiaHitResult.Great: + Health.Value += hpMultiplier * hp_increase_great; + break; + case ManiaHitResult.Perfect: + Health.Value += hpMultiplier * hp_increase_perfect; + break; + } + + // A factor that is applied to make higher combos more relevant + double comboRelevance = Math.Min(Math.Max(0.5, Math.Log(Combo.Value, combo_base)), Math.Log(combo_relevance_cap, combo_base)); + comboPortion += judgement.ResultValueForScore * comboRelevance; + } + break; + } + + int scoreForAccuracy = 0; + int maxScoreForAccuracy = 0; + + foreach (var j in Judgements) + { + scoreForAccuracy += j.ResultValueForAccuracy; + maxScoreForAccuracy += j.MaxResultValueForAccuracy; + } + + Accuracy.Value = (double)scoreForAccuracy / maxScoreForAccuracy; + TotalScore.Value = comboScore + accuracyScore + bonusScore; } protected override void Reset() @@ -28,6 +286,10 @@ namespace osu.Game.Rulesets.Mania.Scoring base.Reset(); Health.Value = 1; + + bonusScore = 0; + comboPortion = 0; + totalHits = 0; } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs index 95b7979e43..57477147d5 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using System.Linq; using OpenTK; using OpenTK.Input; +using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Lists; +using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Beatmaps; @@ -85,6 +87,34 @@ namespace osu.Game.Rulesets.Mania.UI }; } + [BackgroundDependencyLoader] + private void load() + { + var maniaPlayfield = (ManiaPlayfield)Playfield; + + double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; + + SortedList timingPoints = Beatmap.ControlPointInfo.TimingPoints; + for (int i = 0; i < timingPoints.Count; i++) + { + TimingControlPoint point = timingPoints[i]; + + // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature; + + int index = 0; + for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++) + { + maniaPlayfield.Add(new DrawableBarLine(new BarLine + { + StartTime = t, + ControlPoint = point, + BeatIndex = index + })); + } + } + } + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index ff763f87c4..2e6b63579e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mania.Timing; using osu.Framework.Input; using osu.Framework.Graphics.Transforms; using osu.Framework.MathUtils; +using osu.Game.Rulesets.Mania.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { @@ -57,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly FlowContainer columns; public IEnumerable Columns => columns.Children; - private readonly ControlPointContainer barlineContainer; + private readonly ControlPointContainer barLineContainer; private List normalColumnColours = new List(); private Color4 specialColumnColour; @@ -77,35 +78,51 @@ namespace osu.Game.Rulesets.Mania.UI { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Masking = true, Children = new Drawable[] { - new Box + new Container { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black - }, - columns = new FillFlowContainer - { - Name = "Columns", + Name = "Masked elements", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Left = 1, Right = 1 }, - Spacing = new Vector2(1, 0) + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + columns = new FillFlowContainer + { + Name = "Columns", + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 1, Right = 1 }, + Spacing = new Vector2(1, 0) + } + } }, new Container { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, Children = new[] { - barlineContainer = new ControlPointContainer(timingChanges) + barLineContainer = new ControlPointContainer(timingChanges) { Name = "Bar lines", - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y + // Width is set in the Update method } } } @@ -190,6 +207,7 @@ namespace osu.Game.Rulesets.Mania.UI } public override void Add(DrawableHitObject h) => Columns.ElementAt(h.HitObject.Column).Add(h); + public void Add(DrawableBarLine barline) => barLineContainer.Add(barline); protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { @@ -224,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.UI timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); - barlineContainer.TimeSpan = value; + barLineContainer.TimeSpan = value; Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value); } } @@ -234,6 +252,13 @@ namespace osu.Game.Rulesets.Mania.UI TransformTo(() => TimeSpan, newTimeSpan, duration, easing, new TransformTimeSpan()); } + protected override void Update() + { + // Due to masking differences, it is not possible to get the width of the columns container automatically + // While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually + barLineContainer.Width = columns.Width; + } + private class TransformTimeSpan : Transform { public override double CurrentValue diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 7a8ec25fe4..3d5614bd90 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -62,6 +62,7 @@ + @@ -70,6 +71,7 @@ + diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 831b9082bd..f7df67b66d 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -27,6 +27,7 @@ namespace osu.Game.Overlays.Mods public class ModButton : ModButtonEmpty, IHasTooltip { private ModIcon foregroundIcon; + private ModIcon backgroundIcon; private readonly SpriteText text; private readonly Container iconsContainer; private SampleChannel sampleOn, sampleOff; @@ -35,38 +36,67 @@ namespace osu.Game.Overlays.Mods public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; - private int _selectedIndex = -1; - private int selectedIndex + private const EasingTypes mod_switch_easing = EasingTypes.InOutSine; + private const double mod_switch_duration = 120; + + // A selected index of -1 means not selected. + private int selectedIndex = -1; + + protected int SelectedIndex { get { - return _selectedIndex; + return selectedIndex; } set { - if (value == _selectedIndex) return; - _selectedIndex = value; + if (value == selectedIndex) return; + + int direction = value < selectedIndex ? -1 : 1; + bool beforeSelected = Selected; + + Mod modBefore = SelectedMod ?? Mods[0]; if (value >= Mods.Length) + selectedIndex = -1; + else if (value < -1) + selectedIndex = Mods.Length - 1; + else + selectedIndex = value; + + Mod modAfter = SelectedMod ?? Mods[0]; + + if (beforeSelected != Selected) { - _selectedIndex = -1; - } - else if (value <= -2) - { - _selectedIndex = Mods.Length - 1; + iconsContainer.RotateTo(Selected ? 5f : 0f, 300, EasingTypes.OutElastic); + iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, EasingTypes.OutElastic); + } + + if (modBefore != modAfter) + { + const float rotate_angle = 16; + + foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); + backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); + + backgroundIcon.Icon = modAfter.Icon; + using (iconsContainer.BeginDelayedSequence(mod_switch_duration, true)) + { + foregroundIcon.RotateTo(-rotate_angle * direction); + foregroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); + + backgroundIcon.RotateTo(rotate_angle * direction); + backgroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); + + iconsContainer.Schedule(() => displayMod(modAfter)); + } } - iconsContainer.RotateTo(Selected ? 5f : 0f, 300, EasingTypes.OutElastic); - iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, EasingTypes.OutElastic); foregroundIcon.Highlighted = Selected; - - if (mod != null) - displayMod(SelectedMod ?? Mods[0]); } } - public bool Selected => selectedIndex != -1; - + public bool Selected => SelectedIndex != -1; private Color4 selectedColour; public Color4 SelectedColour @@ -117,7 +147,7 @@ namespace osu.Game.Overlays.Mods // the mods from Mod, only multiple if Mod is a MultiMod - public override Mod SelectedMod => Mods.ElementAtOrDefault(selectedIndex); + public override Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex); [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -142,23 +172,25 @@ namespace osu.Game.Overlays.Mods public void SelectNext() { - (++selectedIndex == -1 ? sampleOff : sampleOn).Play(); + (++SelectedIndex == -1 ? sampleOff : sampleOn).Play(); Action?.Invoke(SelectedMod); } public void SelectPrevious() { - (--selectedIndex == -1 ? sampleOff : sampleOn).Play(); + (--SelectedIndex == -1 ? sampleOff : sampleOn).Play(); Action?.Invoke(SelectedMod); } public void Deselect() { - selectedIndex = -1; + SelectedIndex = -1; } private void displayMod(Mod mod) { + if (backgroundIcon != null) + backgroundIcon.Icon = foregroundIcon.Icon; foregroundIcon.Icon = mod.Icon; text.Text = mod.Name; } @@ -170,17 +202,17 @@ namespace osu.Game.Overlays.Mods { iconsContainer.Add(new[] { - new ModIcon(Mods[0]) + backgroundIcon = new ModIcon(Mods[1]) { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Position = new Vector2(1.5f), }, foregroundIcon = new ModIcon(Mods[0]) { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Position = new Vector2(-1.5f), },