diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index e04a30d06c..f46573c494 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Bindables; using osu.Game.Configuration; @@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset { + public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) }; + [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs new file mode 100644 index 0000000000..ee325db66a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -0,0 +1,148 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Threading; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset + { + public override string Name => @"Strict Tracking"; + public override string Acronym => @"ST"; + public override IconUsage? Icon => FontAwesome.Solid.PenFancy; + public override ModType Type => ModType.DifficultyIncrease; + public override string Description => @"Follow circles just got serious..."; + public override double ScoreMultiplier => 1.0; + public override Type[] IncompatibleMods => new[] { typeof(ModClassic) }; + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + if (drawable is DrawableSlider slider) + { + slider.Tracking.ValueChanged += e => + { + if (e.NewValue || slider.Judged) return; + + var tail = slider.NestedHitObjects.OfType().First(); + + if (!tail.Judged) + tail.MissForcefully(); + }; + } + } + + public void ApplyToBeatmap(IBeatmap beatmap) + { + var osuBeatmap = (OsuBeatmap)beatmap; + + if (osuBeatmap.HitObjects.Count == 0) return; + + var hitObjects = osuBeatmap.HitObjects.Select(ho => + { + if (ho is Slider slider) + { + var newSlider = new StrictTrackingSlider(slider); + return newSlider; + } + + return ho; + }).ToList(); + + osuBeatmap.HitObjects = hitObjects; + } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.Playfield.RegisterPool(10, 100); + } + + private class StrictTrackingSliderTailCircle : SliderTailCircle + { + public StrictTrackingSliderTailCircle(Slider slider) + : base(slider) + { + } + + public override Judgement CreateJudgement() => new OsuJudgement(); + } + + private class StrictTrackingDrawableSliderTail : DrawableSliderTail + { + public override bool DisplayResult => true; + } + + private class StrictTrackingSlider : Slider + { + public StrictTrackingSlider(Slider original) + { + StartTime = original.StartTime; + Samples = original.Samples; + Path = original.Path; + NodeSamples = original.NodeSamples; + RepeatCount = original.RepeatCount; + Position = original.Position; + NewCombo = original.NewCombo; + ComboOffset = original.ComboOffset; + LegacyLastTickOffset = original.LegacyLastTickOffset; + TickDistanceMultiplier = original.TickDistanceMultiplier; + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + + foreach (var e in sliderEvents) + { + switch (e.Type) + { + case SliderEventType.Head: + AddNested(HeadCircle = new SliderHeadCircle + { + StartTime = e.Time, + Position = Position, + StackHeight = StackHeight, + }); + break; + + case SliderEventType.LegacyLastTick: + AddNested(TailCircle = new StrictTrackingSliderTailCircle(this) + { + RepeatIndex = e.SpanIndex, + StartTime = e.Time, + Position = EndPosition, + StackHeight = StackHeight + }); + break; + + case SliderEventType.Repeat: + AddNested(new SliderRepeat(this) + { + RepeatIndex = e.SpanIndex, + StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, + Position = Position + Path.PositionAt(e.PathProgress), + StackHeight = StackHeight, + Scale = Scale, + }); + break; + } + } + + UpdateNestedSamples(); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 776165cfb4..a698311bf7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects public Slider() { - SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples(); + SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples(); Path.Version.ValueChanged += _ => updateNestedPositions(); } @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects } } - updateNestedSamples(); + UpdateNestedSamples(); } private void updateNestedPositions() @@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects TailCircle.Position = EndPosition; } - private void updateNestedSamples() + protected void UpdateNestedSamples() { var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 2fdf42fca1..47a2618ddd 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new OsuModHidden(), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), + new OsuModStrictTracking() }; case ModType.Conversion: diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index cd8f99db8b..ea5ffb10c6 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI /// /// The type. /// The receiver for s. - protected void RegisterPool(int initialSize, int? maximumSize = null) + public void RegisterPool(int initialSize, int? maximumSize = null) where TObject : HitObject where TDrawable : DrawableHitObject, new() => RegisterPool(new DrawablePool(initialSize, maximumSize));