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),
},