diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs index a81ebcbcd..d255e5cd8 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.HitCircle.cs @@ -7,16 +7,19 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; public partial class SentakkiBeatmapConverter { - private Tap convertHitCircle(HitObject original) + private Tap convertHitCircle(HitObject original) => convertHitCircle(original, currentLane, original.StartTime); + private Tap convertHitCircle(HitObject original, int lane, double startTime) { bool isBreak = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE); Tap result = new Tap { - Lane = currentLane.NormalizePath(), + Lane = lane, Samples = original.Samples, - StartTime = original.StartTime, - Break = isBreak + StartTime = startTime, + Break = isBreak, + Ex = isSoft }; return result; diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs index ea945faff..0a9ca0e04 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.Slider.cs @@ -14,7 +14,8 @@ namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; public partial class SentakkiBeatmapConverter { - private SentakkiHitObject convertSlider(HitObject original) + private SentakkiHitObject convertSlider(HitObject original) => convertSlider(original, currentLane, false, true); + private SentakkiHitObject convertSlider(HitObject original, int lane, bool forceHoldNote, bool allowFans) { double duration = ((IHasDuration)original).Duration; @@ -22,38 +23,41 @@ private SentakkiHitObject convertSlider(HitObject original) bool isSuitableSlider = !isLazySlider(original); - bool isBreak = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_FINISH); - - if (isSuitableSlider) + if (isSuitableSlider && !forceHoldNote) { - var slide = tryConvertToSlide(original, currentLane); + var slide = tryConvertToSlide(original, lane, allowFans); if (slide is not null) return slide.Value.Item1; } + bool isBreak = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = slider.NodeSamples[0].Any(s => s.Name == HitSampleInfo.HIT_WHISTLE); + var hold = new Hold { - Lane = currentLane = currentLane.NormalizePath(), + Lane = lane, Break = isBreak, StartTime = original.StartTime, Duration = duration, NodeSamples = slider.NodeSamples, + Ex = isSoft, }; return hold; } - private (Slide, int endLane)? tryConvertToSlide(HitObject original, int lane) + private (Slide, int endLane)? tryConvertToSlide(HitObject original, int lane, bool allowFans) { var nodeSamples = ((IHasPathWithRepeats)original).NodeSamples; - var selectedPath = chooseSlidePartFor(original); + var selectedPath = chooseSlidePartFor(original, allowFans); if (selectedPath is null) return null; bool tailBreak = nodeSamples.Last().Any(s => s.Name == HitSampleInfo.HIT_FINISH); bool headBreak = nodeSamples.First().Any(s => s.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = original.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE); int endOffset = selectedPath.Sum(p => p.EndOffset); @@ -74,20 +78,21 @@ private SentakkiHitObject convertSlider(HitObject original) Lane = lane.NormalizePath(), StartTime = original.StartTime, Samples = nodeSamples.FirstOrDefault(), - Break = headBreak + Break = headBreak, + Ex = isSoft }; return (slide, end); } - private SlideBodyPart[]? chooseSlidePartFor(HitObject original) + private SlideBodyPart[]? chooseSlidePartFor(HitObject original, bool allowFans) { double velocity = original is IHasSliderVelocity slider ? (slider.SliderVelocityMultiplier * beatmap.Difficulty.SliderMultiplier) : 1; double duration = ((IHasDuration)original).Duration; double adjustedDuration = duration * velocity; var candidates = SlidePaths.VALIDPATHS.AsEnumerable(); - if (!ConversionFlags.HasFlag(ConversionFlags.fanSlides)) + if (!ConversionFlags.HasFlag(ConversionFlags.fanSlides) || !allowFans) candidates = candidates.Where(p => p.SlidePart.Shape != SlidePaths.PathShapes.Fan); if (!ConversionFlags.HasFlag(ConversionFlags.disableCompositeSlides)) diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs index 3854778c5..2c5525aac 100644 --- a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/SentakkiBeatmapConverter.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -25,18 +27,22 @@ public partial class SentakkiBeatmapConverter : BeatmapConverter ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { - SentakkiHitObject result = original switch + SentakkiHitObject result; + switch (original) { - IHasPathWithRepeats => convertSlider(original), - IHasDuration => convertSpinner(original), - _ => convertHitCircle(original) - }; + case IHasPathWithRepeats s: + result = convertSlider(original); + break; + case IHasDuration: + result = convertSpinner(original); + break; + default: + result = convertHitCircle(original); + break; + } + + // Twin note generation section + if (ConversionFlags.HasFlagFast(ConversionFlags.twinNotes)) + { + switch (original) + { + case IHasPathWithRepeats s: + bool allClaps = s.NodeSamples.All(ns => ns.Any(h => h.Name == HitSampleInfo.HIT_CLAP)); + + double fanStartTime = double.MaxValue; + if (result is Slide slide) + { + var slidePath = slide.SlideInfoList[0].SlidePath; + if (slidePath.EndsWithSlideFan) + fanStartTime = slide.StartTime + (slide.Duration * slidePath.FanStartProgress); + } + + if (allClaps && fanStartTime == double.MaxValue) + { + if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) + yield return convertSlider(original, twinLane, !ConversionFlags.HasFlagFast(ConversionFlags.twinSlides), false); + break; + } + + // Fallback to using taps for each node with a clap + double spansDuration = s.Duration / (s.RepeatCount + 1); + + for (int i = 0; i < s.NodeSamples.Count; ++i) + { + var samples = s.NodeSamples[i]; + if (samples.All(h => h.Name != HitSampleInfo.HIT_CLAP)) + continue; + + double targetTime = original.StartTime + (spansDuration * i); + + if (targetTime >= fanStartTime) + break; + + bool isBreak = samples.Any(h => h.Name == HitSampleInfo.HIT_FINISH); + bool isSoft = samples.Any(h => h.Name == HitSampleInfo.HIT_WHISTLE); + + if (tryGetLaneForTwinNote(targetTime, out int twinLane)) + { + var sho = (SentakkiLanedHitObject)convertHitCircle(original, twinLane, targetTime); + sho.Break = isBreak; + sho.Samples = samples; + sho.Ex = isSoft; + + yield return sho; + } + } + + break; + default: + if (original.Samples.Any(h => h.Name == HitSampleInfo.HIT_CLAP)) + { + if (tryGetLaneForTwinNote(original.StartTime, out int twinLane)) + yield return convertHitCircle(original, twinLane, original.StartTime); + } + break; + } + } // Update the lane to be used by the next hitobject updateCurrentLane(original, result); @@ -73,6 +161,9 @@ private void updateCurrentLane(HitObject original, SentakkiHitObject converted) if (next is null) return; + if (((IHasCombo)next).NewCombo) + newComboSinceLastTwin = true; + // If the next note is far off, we start from a fresh slate if (!isChronologicallyClose(original, next)) { @@ -228,11 +319,15 @@ private static int getClosestLaneFor(float angle) return closestLane; } - private bool isChronologicallyClose(HitObject a, HitObject b) + private bool isChronologicallyClose(double a, double b) { - double timeDelta = b.StartTime - a.GetEndTime(); - double beatLength = beatmap.ControlPointInfo.TimingPointAt(b.StartTime).BeatLength; + double timeDelta = b - a; + double beatLength = beatmap.ControlPointInfo.TimingPointAt(b).BeatLength; return timeDelta <= beatLength; } + private bool isChronologicallyClose(HitObject a, HitObject b) + { + return isChronologicallyClose(a.GetEndTime(), b.StartTime); + } } diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs new file mode 100644 index 000000000..5be440bbf --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinFlags.cs @@ -0,0 +1,14 @@ +using System; + +namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; + +[Flags] +public enum TwinFlags +{ + None = 0, + Mirror = 1 << 1, // The lane is horizontally mirrored from the main note + Cycle = 1 << 2, // Cycles between 1 or more different lanes, prechosen + SpinCW = 1 << 3, // Increments lane by 1 clockwise + SpinCCW = 1 << 4, // Decrements lane by 1 counterclockwise + Copy = 1 << 5, // Simply copies the main note, but with an offset +} diff --git a/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs new file mode 100644 index 000000000..2988e3852 --- /dev/null +++ b/osu.Game.Rulesets.Sentakki/Beatmaps/Converter/TwinPattern.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Markdig.Extensions.Yaml; +using osu.Framework.Extensions.EnumExtensions; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Sentakki.Objects; +using osu.Game.Beatmaps; + +namespace osu.Game.Rulesets.Sentakki.Beatmaps.Converter; + +public class TwinPattern +{ + private static readonly TwinFlags[] allowedFlags = new TwinFlags[]{ + TwinFlags.None, + TwinFlags.SpinCW, + TwinFlags.SpinCCW, + TwinFlags.Cycle, + TwinFlags.Copy, + TwinFlags.Mirror, + TwinFlags.Copy | TwinFlags.Mirror + }; + + private TwinFlags flags; + + private List cycleLanes = new List(); + private int cycleIndex = 0; + + private int originLane = 0; + + private int spinIncrement = 0; + + private int copyOffset = 1; + + private Random rng; + + public TwinPattern(Random rng) + { + this.rng = rng; + + NewPattern(); + } + + public void NewPattern() + { + flags = allowedFlags[rng.Next(0, allowedFlags.Length)]; + originLane = rng.Next(0, 8); + + if (flags.HasFlagFast(TwinFlags.Cycle)) + { + cycleLanes.Clear(); + cycleLanes.Add(rng.Next(0, 8)); + cycleIndex = 0; + + float prob = 0.75f; + + while (true) + { + if (rng.NextSingle() > prob) + break; + + cycleLanes.Add(rng.Next(0, 8)); + prob *= 0.5f; + } + } + else if (flags.HasFlagFast(TwinFlags.Copy)) + { + copyOffset = rng.Next(1, 7); + } + } + + public int getNextLane(int currentLane) + { + if (flags.HasFlagFast(TwinFlags.Cycle)) + { + int tmp = originLane + cycleLanes[cycleIndex]; + cycleIndex = (cycleIndex + 1) % cycleLanes.Count; + + return tmp; + } + + if (flags.HasFlagFast(TwinFlags.SpinCW)) + return originLane + (++spinIncrement); + + if (flags.HasFlagFast(TwinFlags.SpinCCW)) + return originLane + (--spinIncrement); + + + int result = currentLane; + if (flags.HasFlagFast(TwinFlags.Copy)) + result += copyOffset; + + if (flags.HasFlagFast(TwinFlags.Mirror)) + result = 7 - result; + + return result.NormalizePath(); + } +} diff --git a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs index b46fa848d..acaecbb74 100644 --- a/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs +++ b/osu.Game.Rulesets.Sentakki/Mods/SentakkiModExperimental.cs @@ -42,10 +42,8 @@ public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter) if (EnableSlideFans.Value) sentakkiBeatmapConverter.flags |= ConversionFlags.fanSlides; - if (!OldConversion.Value) - return; - - sentakkiBeatmapConverter.flags |= ConversionFlags.oldConverter; + if (OldConversion.Value) + sentakkiBeatmapConverter.flags |= ConversionFlags.oldConverter; if (EnableTwinNotes.Value) sentakkiBeatmapConverter.flags |= ConversionFlags.twinNotes;