public void TestLegacyLastTickOffset()
        {
            var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();

            Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
            Assert.That(events[2].Time, Is.EqualTo(900));
        }
        public void TestNonEvenTicks()
        {
            var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();

            Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
            Assert.That(events[0].Time, Is.EqualTo(start_time));

            Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[1].Time, Is.EqualTo(300));

            Assert.That(events[2].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[2].Time, Is.EqualTo(600));

            Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[3].Time, Is.EqualTo(900));

            Assert.That(events[4].Type, Is.EqualTo(SliderEventType.Repeat));
            Assert.That(events[4].Time, Is.EqualTo(span_duration));

            Assert.That(events[5].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[5].Time, Is.EqualTo(1100));

            Assert.That(events[6].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[6].Time, Is.EqualTo(1400));

            Assert.That(events[7].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[7].Time, Is.EqualTo(1700));

            Assert.That(events[9].Type, Is.EqualTo(SliderEventType.Tail));
            Assert.That(events[9].Time, Is.EqualTo(2 * span_duration));
        }
Exemple #3
0
            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.Tick:
                        AddNested(new SliderTick
                        {
                            SpanIndex     = e.SpanIndex,
                            SpanStartTime = e.SpanStartTime,
                            StartTime     = e.Time,
                            Position      = Position + Path.PositionAt(e.PathProgress),
                            StackHeight   = StackHeight,
                            Scale         = Scale,
                        });
                        break;

                    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();
            }
Exemple #4
0
        public static List <SentakkiHitObject> CreateTapFromTicks(HitObject original, int path, IBeatmap beatmap, Random rng)
        {
            var    curve        = original as IHasCurve;
            double spanDuration = curve.Duration / (curve.RepeatCount + 1);
            bool   isRepeatSpam = spanDuration < 75 && curve.RepeatCount > 0;

            List <SentakkiHitObject> hitObjects = new List <SentakkiHitObject>();

            if (isRepeatSpam)
            {
                return(hitObjects);
            }

            var difficulty = beatmap.BeatmapInfo.BaseDifficulty;

            var controlPointInfo = beatmap.ControlPointInfo;
            TimingControlPoint     timingPoint     = controlPointInfo.TimingPointAt(original.StartTime);
            DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(original.StartTime);

            double scoringDistance = 100 * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;

            var velocity     = scoringDistance / timingPoint.BeatLength;
            var tickDistance = scoringDistance / difficulty.SliderTickRate;

            double legacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

            foreach (var e in SliderEventGenerator.Generate(original.StartTime, spanDuration, velocity, tickDistance, curve.Path.Distance, curve.RepeatCount + 1, legacyLastTickOffset))
            {
                int newPath = path;
                while (newPath == path)
                {
                    newPath = rng.Next(0, 8);
                }

                switch (e.Type)
                {
                case SliderEventType.Tick:
                case SliderEventType.Repeat:
                    hitObjects.Add(new Tap
                    {
                        NoteColor   = Color4.Orange,
                        Angle       = newPath.GetAngleFromPath(),
                        Samples     = getTickSamples(original.Samples),
                        StartTime   = e.Time,
                        EndPosition = SentakkiExtensions.GetPosition(SentakkiPlayfield.INTERSECTDISTANCE, newPath),
                        Position    = SentakkiExtensions.GetPosition(SentakkiPlayfield.NOTESTARTDISTANCE, newPath),
                    });
                    break;
                }
            }
            return(hitObjects);
        }
        public void TestSingleSpan()
        {
            var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();

            Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
            Assert.That(events[0].Time, Is.EqualTo(start_time));

            Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[1].Time, Is.EqualTo(span_duration / 2));

            Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tail));
            Assert.That(events[3].Time, Is.EqualTo(span_duration));
        }
Exemple #6
0
        private IEnumerable <SentakkiHitObject> createTapsFromTicks(HitObject original)
        {
            int noteLane = getNewLane(true);

            var    curve        = original as IHasPathWithRepeats;
            double spanDuration = curve.Duration / (curve.RepeatCount + 1);
            bool   isRepeatSpam = spanDuration < 75 && curve.RepeatCount > 0;

            if (isRepeatSpam)
            {
                yield break;
            }

            var difficulty = beatmap.BeatmapInfo.BaseDifficulty;

            var controlPointInfo = beatmap.ControlPointInfo;
            TimingControlPoint     timingPoint     = controlPointInfo.TimingPointAt(original.StartTime);
            DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(original.StartTime);

            double scoringDistance = 100 * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;

            var velocity     = scoringDistance / timingPoint.BeatLength;
            var tickDistance = scoringDistance / difficulty.SliderTickRate;

            double legacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

            foreach (var e in SliderEventGenerator.Generate(original.StartTime, spanDuration, velocity, tickDistance, curve.Path.Distance, curve.RepeatCount + 1, legacyLastTickOffset, CancellationToken.None))
            {
                switch (e.Type)
                {
                case SliderEventType.Tick:
                case SliderEventType.Repeat:
                    yield return(new Tap
                    {
                        Lane = noteLane,
                        Samples = original.Samples.Select(s => new HitSampleInfo(@"slidertick", s.Bank, s.Suffix, s.Volume)).ToList(),
                        StartTime = e.Time
                    });

                    break;
                }
            }
        }
        public void TestRepeat()
        {
            var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, default).ToArray();

            Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
            Assert.That(events[0].Time, Is.EqualTo(start_time));

            Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[1].Time, Is.EqualTo(span_duration / 2));

            Assert.That(events[2].Type, Is.EqualTo(SliderEventType.Repeat));
            Assert.That(events[2].Time, Is.EqualTo(span_duration));

            Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tick));
            Assert.That(events[3].Time, Is.EqualTo(span_duration + span_duration / 2));

            Assert.That(events[5].Type, Is.EqualTo(SliderEventType.Tail));
            Assert.That(events[5].Time, Is.EqualTo(2 * span_duration));
        }
        public static IEnumerable <BosuHitObject> ConvertBuzzSlider(HitObject obj, Vector2 originalPosition, IBeatmap beatmap, IHasPathWithRepeats curve, double spanDuration)
        {
            List <BosuHitObject> converted = new List <BosuHitObject>();

            var difficulty = beatmap.BeatmapInfo.BaseDifficulty;

            var controlPointInfo = beatmap.ControlPointInfo;
            TimingControlPoint     timingPoint     = controlPointInfo.TimingPointAt(obj.StartTime);
            DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(obj.StartTime);

            double scoringDistance = 100 * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;

            var velocity     = scoringDistance / timingPoint.BeatLength;
            var tickDistance = scoringDistance / difficulty.SliderTickRate;

            double legacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

            foreach (var e in SliderEventGenerator.Generate(obj.StartTime, spanDuration, velocity, tickDistance, curve.Path.Distance, curve.RepeatCount + 1, legacyLastTickOffset, new CancellationToken()))
            {
                var sliderEventPosition = toPlayfieldSpace(originalPosition) * new Vector2(1, 0.4f);

                switch (e.Type)
                {
                case SliderEventType.Head:
                    converted.AddRange(generateExplosion(e.Time, bullets_per_slider_reverse, sliderEventPosition));
                    break;

                case SliderEventType.Repeat:
                    converted.AddRange(generateExplosion(e.Time, bullets_per_slider_reverse, sliderEventPosition, slider_angle_per_span * (e.SpanIndex + 1)));
                    break;

                case SliderEventType.Tail:
                    converted.AddRange(generateExplosion(e.Time, bullets_per_slider_reverse, sliderEventPosition, slider_angle_per_span * (curve.RepeatCount + 1)));
                    break;
                }
            }

            return(converted);
        }
        public void TestMinimumTickDistance()
        {
            const double velocity     = 5;
            const double min_distance = velocity * 10;

            var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();

            Assert.Multiple(() =>
            {
                int tickIndex = -1;

                while (++tickIndex < events.Length)
                {
                    if (events[tickIndex].Type != SliderEventType.Tick)
                    {
                        continue;
                    }

                    Assert.That(events[tickIndex].Time, Is.LessThan(span_duration - min_distance).Or.GreaterThan(span_duration + min_distance));
                }
            });
        }
        public static IEnumerable <BosuHitObject> ConvertDefaultSlider(HitObject obj, Vector2 originalPosition, IBeatmap beatmap, IHasPathWithRepeats curve, double spanDuration)
        {
            List <BosuHitObject> converted = new List <BosuHitObject>();

            var difficulty = beatmap.BeatmapInfo.BaseDifficulty;

            var controlPointInfo = beatmap.ControlPointInfo;
            TimingControlPoint     timingPoint     = controlPointInfo.TimingPointAt(obj.StartTime);
            DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(obj.StartTime);

            double scoringDistance = 100 * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;

            var velocity     = scoringDistance / timingPoint.BeatLength;
            var tickDistance = scoringDistance / difficulty.SliderTickRate;

            double legacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

            foreach (var e in SliderEventGenerator.Generate(obj.StartTime, spanDuration, velocity, tickDistance, curve.Path.Distance, curve.RepeatCount + 1, legacyLastTickOffset, new CancellationToken()))
            {
                var curvePosition       = curve.CurvePositionAt(e.PathProgress / (curve.RepeatCount + 1)) + originalPosition;
                var sliderEventPosition = toPlayfieldSpace(curvePosition) * new Vector2(1, 0.4f);

                switch (e.Type)
                {
                case SliderEventType.Repeat:
                    converted.AddRange(generateExplosion(e.Time, Math.Clamp((int)curve.Distance / 15, 3, 15), sliderEventPosition, MathExtensions.GetRandomTimedAngleOffset(e.Time)));
                    break;

                case SliderEventType.Tail:
                    converted.AddRange(generateExplosion(e.Time, Math.Clamp((int)curve.Distance * (curve.RepeatCount + 1) / 15, 5, 20), sliderEventPosition, MathExtensions.GetRandomTimedAngleOffset(e.Time)));
                    break;
                }
            }

            return(converted);
        }
Exemple #11
0
        protected override void CreateNestedHitObjects()
        {
            base.CreateNestedHitObjects();

            var tickSamples = Samples.Select(s => new HitSampleInfo
            {
                Bank   = s.Bank,
                Name   = @"slidertick",
                Volume = s.Volume
            }).ToList();

            SliderEventDescriptor?lastEvent = null;

            foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
            {
                // generate tiny droplets since the last point
                if (lastEvent != null)
                {
                    double sinceLastTick = e.Time - lastEvent.Value.Time;

                    if (sinceLastTick > 80)
                    {
                        double timeBetweenTiny = sinceLastTick;
                        while (timeBetweenTiny > 100)
                        {
                            timeBetweenTiny /= 2;
                        }

                        for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
                        {
                            AddNested(new TinyDroplet
                            {
                                Samples   = tickSamples,
                                StartTime = t + lastEvent.Value.Time,
                                X         = X + Path.PositionAt(
                                    lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
                            });
                        }
                    }
                }

                // this also includes LegacyLastTick and this is used for TinyDroplet generation above.
                // this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied.
                lastEvent = e;

                switch (e.Type)
                {
                case SliderEventType.Tick:
                    AddNested(new Droplet
                    {
                        Samples   = tickSamples,
                        StartTime = e.Time,
                        X         = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
                    });
                    break;

                case SliderEventType.Head:
                case SliderEventType.Tail:
                case SliderEventType.Repeat:
                    AddNested(new Fruit
                    {
                        Samples   = Samples,
                        StartTime = e.Time,
                        X         = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
                    });
                    break;
                }
            }
        }
Exemple #12
0
        protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
        {
            base.CreateNestedHitObjects(cancellationToken);

            var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();

            int nodeIndex = 0;
            SliderEventDescriptor?lastEvent = null;

            foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
            {
                // generate tiny droplets since the last point
                if (lastEvent != null)
                {
                    double sinceLastTick = e.Time - lastEvent.Value.Time;

                    if (sinceLastTick > 80)
                    {
                        double timeBetweenTiny = sinceLastTick;
                        while (timeBetweenTiny > 100)
                        {
                            timeBetweenTiny /= 2;
                        }

                        for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
                        {
                            cancellationToken.ThrowIfCancellationRequested();

                            AddNested(new TinyDroplet
                            {
                                StartTime = t + lastEvent.Value.Time,
                                X         = X + Path.PositionAt(
                                    lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
                            });
                        }
                    }
                }

                // this also includes LegacyLastTick and this is used for TinyDroplet generation above.
                // this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied.
                lastEvent = e;

                switch (e.Type)
                {
                case SliderEventType.Tick:
                    AddNested(new Droplet
                    {
                        Samples   = dropletSamples,
                        StartTime = e.Time,
                        X         = X + Path.PositionAt(e.PathProgress).X,
                    });
                    break;

                case SliderEventType.Head:
                case SliderEventType.Tail:
                case SliderEventType.Repeat:
                    AddNested(new Fruit
                    {
                        Samples   = this.GetNodeSamples(nodeIndex++),
                        StartTime = e.Time,
                        X         = X + Path.PositionAt(e.PathProgress).X,
                    });
                    break;
                }
            }
        }
Exemple #13
0
        protected override IEnumerable <BosuHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
        {
            var objPosition = (obj as IHasPosition)?.Position ?? Vector2.Zero;
            var comboData   = obj as IHasCombo;
            var difficulty  = beatmap.BeatmapInfo.BaseDifficulty;

            if (comboData?.NewCombo ?? false)
            {
                index++;
            }

            List <BosuHitObject> hitObjects = new List <BosuHitObject>();

            EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(obj.StartTime);
            bool kiai = effectPoint.KiaiMode;

            switch (obj)
            {
            // Slider
            case IHasCurve curve:
                if (!SlidersOnly)
                {
                    var controlPointInfo = beatmap.ControlPointInfo;
                    TimingControlPoint     timingPoint     = controlPointInfo.TimingPointAt(obj.StartTime);
                    DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(obj.StartTime);

                    double scoringDistance = 100 * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;

                    var velocity     = scoringDistance / timingPoint.BeatLength;
                    var tickDistance = scoringDistance / difficulty.SliderTickRate;

                    double spanDuration         = curve.Duration / (curve.RepeatCount + 1);
                    double legacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

                    foreach (var e in SliderEventGenerator.Generate(obj.StartTime, spanDuration, velocity, tickDistance, curve.Path.Distance, curve.RepeatCount + 1, legacyLastTickOffset))
                    {
                        Vector2 sliderEventPosition;

                        // Don't take into account very small sliders. There's a chance that they will contain reverse spam, and offset looks ugly
                        if (spanDuration < 75)
                        {
                            sliderEventPosition = objPosition * new Vector2(1, 0.5f);
                        }
                        else
                        {
                            sliderEventPosition = (curve.CurvePositionAt(e.PathProgress / (curve.RepeatCount + 1)) + objPosition) * new Vector2(1, 0.5f);
                        }

                        switch (e.Type)
                        {
                        case SliderEventType.Head:
                            hitObjects.AddRange(generateExplosion(
                                                    e.Time,
                                                    kiai ? bullets_per_slider_head_kiai : bullets_per_slider_head,
                                                    sliderEventPosition,
                                                    comboData,
                                                    index));

                            if (Symmetry)
                            {
                                hitObjects.AddRange(generateExplosion(
                                                        e.Time,
                                                        kiai ? bullets_per_slider_head_kiai : bullets_per_slider_head,
                                                        getSymmetricalXPosition(sliderEventPosition),
                                                        comboData,
                                                        index));
                            }

                            hitObjects.Add(new SoundHitObject
                            {
                                StartTime = obj.StartTime,
                                Samples   = obj.Samples
                            });

                            break;

                        case SliderEventType.Tick:
                            hitObjects.Add(new TickCherry
                            {
                                StartTime      = e.Time,
                                Position       = sliderEventPosition,
                                NewCombo       = comboData?.NewCombo ?? false,
                                ComboOffset    = comboData?.ComboOffset ?? 0,
                                IndexInBeatmap = index
                            });

                            if (Symmetry)
                            {
                                hitObjects.Add(new TickCherry
                                {
                                    StartTime      = e.Time,
                                    Position       = getSymmetricalXPosition(sliderEventPosition),
                                    NewCombo       = comboData?.NewCombo ?? false,
                                    ComboOffset    = comboData?.ComboOffset ?? 0,
                                    IndexInBeatmap = index
                                });
                            }

                            hitObjects.Add(new SoundHitObject
                            {
                                StartTime = e.Time,
                                Samples   = getTickSamples(obj.Samples)
                            });
                            break;

                        case SliderEventType.Repeat:
                            hitObjects.AddRange(generateExplosion(
                                                    obj.StartTime + (e.SpanIndex + 1) * spanDuration,
                                                    kiai ? bullets_per_slider_reverse_kiai : bullets_per_slider_reverse,
                                                    sliderEventPosition,
                                                    comboData,
                                                    index,
                                                    slider_angle_per_span * e.SpanIndex));

                            if (Symmetry)
                            {
                                hitObjects.AddRange(generateExplosion(
                                                        obj.StartTime + (e.SpanIndex + 1) * spanDuration,
                                                        kiai ? bullets_per_slider_reverse_kiai : bullets_per_slider_reverse,
                                                        getSymmetricalXPosition(sliderEventPosition),
                                                        comboData,
                                                        index,
                                                        -slider_angle_per_span * e.SpanIndex));
                            }

                            hitObjects.Add(new SoundHitObject
                            {
                                StartTime = e.Time,
                                Samples   = obj.Samples
                            });
                            break;

                        case SliderEventType.Tail:
                            hitObjects.AddRange(generateExplosion(
                                                    e.Time,
                                                    kiai ? bullets_per_slider_tail_kiai : bullets_per_slider_tail,
                                                    sliderEventPosition,
                                                    comboData,
                                                    index));

                            if (Symmetry)
                            {
                                hitObjects.AddRange(generateExplosion(
                                                        e.Time,
                                                        kiai ? bullets_per_slider_tail_kiai : bullets_per_slider_tail,
                                                        getSymmetricalXPosition(sliderEventPosition),
                                                        comboData,
                                                        index));
                            }

                            hitObjects.Add(new SoundHitObject
                            {
                                StartTime = curve.EndTime,
                                Samples   = obj.Samples
                            });
                            break;
                        }
                    }
                }

                //body

                var bodyCherriesCount = Math.Min(curve.Distance * (curve.RepeatCount + 1) / 10, max_visuals_per_slider_span * (curve.RepeatCount + 1));

                for (int i = 0; i < bodyCherriesCount; i++)
                {
                    var progress = (float)i / bodyCherriesCount;
                    var position = (curve.CurvePositionAt(progress) + objPosition);

                    if (!SlidersOnly)
                    {
                        position *= new Vector2(1, 0.5f);

                        hitObjects.Add(new SliderPartCherry
                        {
                            StartTime      = obj.StartTime + curve.Duration * progress,
                            Position       = position,
                            NewCombo       = comboData?.NewCombo ?? false,
                            ComboOffset    = comboData?.ComboOffset ?? 0,
                            IndexInBeatmap = index
                        });

                        if (Symmetry)
                        {
                            hitObjects.Add(new SliderPartCherry
                            {
                                StartTime      = obj.StartTime + curve.Duration * progress,
                                Position       = getSymmetricalXPosition(position),
                                NewCombo       = comboData?.NewCombo ?? false,
                                ComboOffset    = comboData?.ComboOffset ?? 0,
                                IndexInBeatmap = index
                            });
                        }
                    }
                    else
                    {
                        hitObjects.AddRange(new[]
                        {
                            new SliderPartCherry
                            {
                                StartTime      = obj.StartTime + curve.Duration * progress,
                                Position       = position,
                                NewCombo       = comboData?.NewCombo ?? false,
                                ComboOffset    = comboData?.ComboOffset ?? 0,
                                IndexInBeatmap = index
                            },
                            new SliderPartCherry
                            {
                                StartTime      = obj.StartTime + curve.Duration * progress,
                                Position       = getSymmetricalXPosition(position),
                                NewCombo       = comboData?.NewCombo ?? false,
                                ComboOffset    = comboData?.ComboOffset ?? 0,
                                IndexInBeatmap = index
                            },
                            new SliderPartCherry
                            {
                                StartTime      = obj.StartTime + curve.Duration * progress,
                                Position       = getSymmetricalYPosition(position),
                                NewCombo       = comboData?.NewCombo ?? false,
                                ComboOffset    = comboData?.ComboOffset ?? 0,
                                IndexInBeatmap = index
                            },
                            new SliderPartCherry
                            {
                                StartTime      = obj.StartTime + curve.Duration * progress,
                                Position       = getSymmetricalYPosition(getSymmetricalXPosition(position)),
                                NewCombo       = comboData?.NewCombo ?? false,
                                ComboOffset    = comboData?.ComboOffset ?? 0,
                                IndexInBeatmap = index
                            }
                        });
                    }
                }

                break;

            // Spinner
            case IHasEndTime endTime:
                if (SlidersOnly)
                {
                    break;
                }

                var spansPerSpinner = endTime.Duration / spinner_span_delay;

                for (int i = 0; i < spansPerSpinner; i++)
                {
                    hitObjects.AddRange(generateExplosion(
                                            obj.StartTime + i * spinner_span_delay,
                                            kiai ? bullets_per_spinner_span_kiai : bullets_per_spinner_span,
                                            objPosition * new Vector2(1, 0.5f),
                                            comboData,
                                            index,
                                            i * spinner_angle_per_span));
                }

                break;

            // Hitcircle
            default:
                if (SlidersOnly)
                {
                    hitObjects.AddRange(new[]
                    {
                        new SliderPartCherry
                        {
                            StartTime      = obj.StartTime,
                            Position       = objPosition,
                            NewCombo       = comboData?.NewCombo ?? false,
                            ComboOffset    = comboData?.ComboOffset ?? 0,
                            IndexInBeatmap = index
                        },
                        new SliderPartCherry
                        {
                            StartTime      = obj.StartTime,
                            Position       = getSymmetricalXPosition(objPosition),
                            NewCombo       = comboData?.NewCombo ?? false,
                            ComboOffset    = comboData?.ComboOffset ?? 0,
                            IndexInBeatmap = index
                        },
                        new SliderPartCherry
                        {
                            StartTime      = obj.StartTime,
                            Position       = getSymmetricalYPosition(objPosition),
                            NewCombo       = comboData?.NewCombo ?? false,
                            ComboOffset    = comboData?.ComboOffset ?? 0,
                            IndexInBeatmap = index
                        },
                        new SliderPartCherry
                        {
                            StartTime      = obj.StartTime,
                            Position       = getSymmetricalYPosition(getSymmetricalXPosition(objPosition)),
                            NewCombo       = comboData?.NewCombo ?? false,
                            ComboOffset    = comboData?.ComboOffset ?? 0,
                            IndexInBeatmap = index
                        }
                    });
                    break;
                }

                hitObjects.AddRange(generateExplosion(
                                        obj.StartTime,
                                        kiai ? bullets_per_hitcircle_kiai : bullets_per_hitcircle,
                                        objPosition * new Vector2(1, 0.5f),
                                        comboData,
                                        index,
                                        0,
                                        120));

                if (Symmetry)
                {
                    hitObjects.AddRange(generateExplosion(
                                            obj.StartTime,
                                            kiai ? bullets_per_hitcircle_kiai : bullets_per_hitcircle,
                                            getSymmetricalXPosition(objPosition * new Vector2(1, 0.5f)),
                                            comboData,
                                            index,
                                            0,
                                            120));
                }

                hitObjects.Add(new SoundHitObject
                {
                    StartTime = obj.StartTime,
                    Samples   = obj.Samples
                });

                break;
            }

            return(hitObjects);
        }
        private static List <TouhouHitObject> generateRepeatSpamSlider(HitObject obj, IBeatmap beatmap, IHasCurve curve, double spanDuration, bool isKiai, int index)
        {
            List <TouhouHitObject> hitObjects = new List <TouhouHitObject>();

            var objPosition = (obj as IHasPosition)?.Position ?? Vector2.Zero;
            var comboData   = obj as IHasCombo;
            var difficulty  = beatmap.BeatmapInfo.BaseDifficulty;

            var controlPointInfo = beatmap.ControlPointInfo;
            TimingControlPoint     timingPoint     = controlPointInfo.TimingPointAt(obj.StartTime);
            DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(obj.StartTime);

            double scoringDistance = 100 * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;

            var velocity     = scoringDistance / timingPoint.BeatLength;
            var tickDistance = scoringDistance / difficulty.SliderTickRate;

            double legacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

            foreach (var e in SliderEventGenerator.Generate(obj.StartTime, spanDuration, velocity, tickDistance, curve.Path.Distance, curve.RepeatCount + 1, legacyLastTickOffset))
            {
                var sliderEventPosition = objPosition * new Vector2(1, 0.5f);

                switch (e.Type)
                {
                case SliderEventType.Head:

                    if (positionIsValid(sliderEventPosition))
                    {
                        hitObjects.AddRange(generateExplosion(
                                                e.Time,
                                                bullets_per_slider_reverse,
                                                sliderEventPosition,
                                                comboData,
                                                isKiai,
                                                index));
                    }

                    hitObjects.Add(new SoundHitObject
                    {
                        StartTime = obj.StartTime,
                        Samples   = obj.Samples
                    });

                    break;

                case SliderEventType.Repeat:

                    if (positionIsValid(sliderEventPosition))
                    {
                        hitObjects.AddRange(generateExplosion(
                                                e.Time,
                                                bullets_per_slider_reverse,
                                                sliderEventPosition,
                                                comboData,
                                                isKiai,
                                                index,
                                                slider_angle_per_span * (e.SpanIndex + 1)));
                    }

                    hitObjects.Add(new SoundHitObject
                    {
                        StartTime = e.Time,
                        Samples   = obj.Samples
                    });
                    break;

                case SliderEventType.Tail:

                    if (positionIsValid(sliderEventPosition))
                    {
                        hitObjects.AddRange(generateExplosion(
                                                e.Time,
                                                bullets_per_slider_reverse,
                                                sliderEventPosition,
                                                comboData,
                                                isKiai,
                                                index,
                                                slider_angle_per_span * (curve.RepeatCount + 1)));
                    }

                    hitObjects.Add(new SoundHitObject
                    {
                        StartTime = curve.EndTime,
                        Samples   = obj.Samples
                    });
                    break;
                }
            }

            hitObjects.AddRange(generateSliderBody(obj, curve, isKiai, index));

            return(hitObjects);
        }
        private static List <TouhouHitObject> generateDefaultSlider(HitObject obj, IBeatmap beatmap, IHasCurve curve, double spanDuration, bool isKiai, int index)
        {
            List <TouhouHitObject> hitObjects = new List <TouhouHitObject>();

            var objPosition = (obj as IHasPosition)?.Position ?? Vector2.Zero;
            var comboData   = obj as IHasCombo;
            var difficulty  = beatmap.BeatmapInfo.BaseDifficulty;

            var controlPointInfo = beatmap.ControlPointInfo;
            TimingControlPoint     timingPoint     = controlPointInfo.TimingPointAt(obj.StartTime);
            DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(obj.StartTime);

            double scoringDistance = 100 * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;

            var velocity     = scoringDistance / timingPoint.BeatLength;
            var tickDistance = scoringDistance / difficulty.SliderTickRate;

            double legacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0;

            foreach (var e in SliderEventGenerator.Generate(obj.StartTime, spanDuration, velocity, tickDistance, curve.Path.Distance, curve.RepeatCount + 1, legacyLastTickOffset))
            {
                var sliderEventPosition = (curve.CurvePositionAt(e.PathProgress / (curve.RepeatCount + 1)) + objPosition) * new Vector2(1, 0.5f);

                switch (e.Type)
                {
                case SliderEventType.Head:

                    hitObjects.Add(new SoundHitObject
                    {
                        StartTime = obj.StartTime,
                        Samples   = obj.Samples
                    });

                    break;

                case SliderEventType.Tick:

                    if (positionIsValid(sliderEventPosition))
                    {
                        hitObjects.Add(new TickCherry
                        {
                            Angle          = 180,
                            StartTime      = e.Time,
                            Position       = sliderEventPosition,
                            NewCombo       = comboData?.NewCombo ?? false,
                            ComboOffset    = comboData?.ComboOffset ?? 0,
                            IndexInBeatmap = index,
                            IsKiai         = isKiai
                        });
                    }

                    hitObjects.Add(new SoundHitObject
                    {
                        StartTime = e.Time,
                        Samples   = getTickSamples(obj.Samples)
                    });
                    break;

                case SliderEventType.Repeat:

                    if (positionIsValid(sliderEventPosition))
                    {
                        hitObjects.AddRange(generateTriangularExplosion(
                                                e.Time,
                                                20,
                                                sliderEventPosition,
                                                comboData,
                                                isKiai,
                                                index,
                                                MathExtensions.GetRandomTimedAngleOffset(e.Time)));
                    }

                    hitObjects.Add(new SoundHitObject
                    {
                        StartTime = e.Time,
                        Samples   = obj.Samples
                    });
                    break;

                case SliderEventType.Tail:

                    if (positionIsValid(sliderEventPosition))
                    {
                        hitObjects.AddRange(generateExplosion(
                                                e.Time,
                                                Math.Clamp((int)curve.Distance / 15, 5, 20),
                                                sliderEventPosition,
                                                comboData,
                                                isKiai,
                                                index));
                    }

                    hitObjects.Add(new SoundHitObject
                    {
                        StartTime = curve.EndTime,
                        Samples   = obj.Samples
                    });
                    break;
                }
            }

            hitObjects.AddRange(generateSliderBody(obj, curve, isKiai, index));

            return(hitObjects);
        }