Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game.Rulesets.Taiko/Mods/TaikoModSimplifiedRhythm.cs
2263 views
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;

namespace osu.Game.Rulesets.Taiko.Mods
{
    public class TaikoModSimplifiedRhythm : Mod, IApplicableToBeatmap
    {
        public override string Name => "Simplified Rhythm";
        public override string Acronym => "SR";
        public override double ScoreMultiplier => 0.6;
        public override LocalisableString Description => "Simplify tricky rhythms!";
        public override IconUsage? Icon => OsuIcon.ModSimplifiedRhythm;
        public override ModType Type => ModType.DifficultyReduction;

        [SettingSource("1/3 to 1/2 conversion", "Converts 1/3 patterns to 1/2 rhythm.")]
        public Bindable<bool> OneThirdConversion { get; } = new BindableBool();

        [SettingSource("1/6 to 1/4 conversion", "Converts 1/6 patterns to 1/4 rhythm.")]
        public Bindable<bool> OneSixthConversion { get; } = new BindableBool(true);

        [SettingSource("1/8 to 1/4 conversion", "Converts 1/8 patterns to 1/4 rhythm.")]
        public Bindable<bool> OneEighthConversion { get; } = new BindableBool();

        public void ApplyToBeatmap(IBeatmap beatmap)
        {
            var taikoBeatmap = (TaikoBeatmap)beatmap;
            var controlPointInfo = taikoBeatmap.ControlPointInfo;

            Hit[] hits = taikoBeatmap.HitObjects.OfType<Hit>().ToArray();

            if (hits.Length == 0)
                return;

            var conversions = new List<(int, int)>();

            if (OneEighthConversion.Value) conversions.Add((8, 4));
            if (OneSixthConversion.Value) conversions.Add((6, 4));
            if (OneThirdConversion.Value) conversions.Add((3, 2));

            bool inPattern = false;

            foreach ((int baseRhythm, int adjustedRhythm) in conversions)
            {
                int patternStartIndex = 0;

                for (int i = 1; i < hits.Length; i++)
                {
                    double snapValue = getSnapBetweenNotes(controlPointInfo, hits[i - 1], hits[i]);

                    if (inPattern)
                    {
                        // pattern continues
                        if (snapValue == baseRhythm)
                            continue;

                        inPattern = false;
                        processPattern(i);
                    }
                    else
                    {
                        if (snapValue == baseRhythm)
                        {
                            patternStartIndex = i - 1;
                            inPattern = true;
                        }
                    }
                }

                // Process the last pattern if we reached the end of the beatmap and are still in a pattern.
                if (inPattern)
                    processPattern(hits.Length);

                void processPattern(int patternEndIndex)
                {
                    // Iterate through the pattern
                    for (int j = patternStartIndex; j < patternEndIndex; j++)
                    {
                        int indexInPattern = j - patternStartIndex;

                        switch (baseRhythm)
                        {
                            // 1/8: Remove every second note
                            case 8:
                            {
                                if (indexInPattern % 2 == 1)
                                {
                                    taikoBeatmap.HitObjects.Remove(hits[j]);
                                }

                                break;
                            }

                            // 1/6 and 1/3: Remove every second note and adjust time of every third
                            case 6:
                            case 3:
                            {
                                if (indexInPattern % 3 == 1)
                                    taikoBeatmap.HitObjects.Remove(hits[j]);
                                else if (indexInPattern % 3 == 2)
                                    hits[j].StartTime = hits[j - 2].StartTime + controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / adjustedRhythm;

                                break;
                            }

                            default:
                                throw new ArgumentOutOfRangeException(nameof(baseRhythm));
                        }
                    }
                }
            }
        }

        private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote)
        {
            var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime);
            return controlPointInfo.GetClosestBeatDivisor(currentTimingPoint.Time + (nextNote.StartTime - currentNote.StartTime));
        }
    }
}