Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add osu!taiko mod Quarterize #30731

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
133 changes: 133 additions & 0 deletions osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModQuarterize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;

namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public partial class TestSceneTaikoModQuarterize : TaikoModTestScene
{
[Test]
public void TestOneThirdConversion()
{
CreateModTest(new ModTestData
{
Mod = new TaikoModQuarterize
{
OneThirdConversion = { Value = true },
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2333, Type = HitType.Rim }, // mod removes this
new Hit { StartTime = 2666, Type = HitType.Centre }, // mod moves this to 2500
new Hit { StartTime = 3000, Type = HitType.Centre },
new Hit { StartTime = 3500, Type = HitType.Centre },
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1700),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2500, TaikoAction.LeftCentre),
new TaikoReplayFrame(2700),
new TaikoReplayFrame(3000, TaikoAction.LeftCentre),
new TaikoReplayFrame(3200),
new TaikoReplayFrame(3500, TaikoAction.LeftCentre),
new TaikoReplayFrame(3700),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6
});
}

[Test]
public void TestOneSixthConversion() => CreateModTest(new ModTestData
{
Mod = new TaikoModQuarterize
{
OneSixthConversion = { Value = true }
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1250, Type = HitType.Centre },
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 1666, Type = HitType.Rim }, // mod removes this
new Hit { StartTime = 1833, Type = HitType.Centre }, // mod moves this to 1750
new Hit { StartTime = 2000, Type = HitType.Centre },
new Hit { StartTime = 2250, Type = HitType.Centre },
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1450),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1600),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1800),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2200),
new TaikoReplayFrame(2250, TaikoAction.LeftCentre),
new TaikoReplayFrame(2450),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 6
});

[Test]
public void TestOneEighthConversion() => CreateModTest(new ModTestData
{
Mod = new TaikoModQuarterize
{
OneEighthConversion = { Value = true }
},
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject>
{
new Hit { StartTime = 1000, Type = HitType.Centre },
new Hit { StartTime = 1250, Type = HitType.Centre },
new Hit { StartTime = 1500, Type = HitType.Centre },
new Hit { StartTime = 1625, Type = HitType.Rim }, // mod removes this
new Hit { StartTime = 1750, Type = HitType.Centre },
new Hit { StartTime = 2000, Type = HitType.Centre },
},
},
ReplayFrames = new List<ReplayFrame>
{
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1200),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1450),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1700),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1900),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 5
});
}
}
136 changes: 136 additions & 0 deletions osu.Game.Rulesets.Taiko/Mods/TaikoModQuarterize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
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 TaikoModQuarterize : Mod, IApplicableToBeatmap
{
public override string Name => "Quarterize";
public override string Acronym => "QR";
public override double ScoreMultiplier => 0.6;
public override LocalisableString Description => "Simplify tricky rhythms!";
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(false);

[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(false);

public void ApplyToBeatmap(IBeatmap beatmap)
{
var taikoBeatmap = (TaikoBeatmap)beatmap;
var controlPointInfo = taikoBeatmap.ControlPointInfo;
List<Hit> toRemove = new List<Hit>();

// Snap conversions for rhythms
var snapConversions = new Dictionary<int, double>
{
{ 8, 4.0 }, // 1/8 snap to 1/4 snap
{ 6, 4.0 }, // 1/6 snap to 1/4 snap
{ 3, 2.0 }, // 1/3 snap to 1/2 snap
};

bool inPattern = false;

List<Hit> hits = taikoBeatmap.HitObjects.Where(obj => obj is Hit).Cast<Hit>().ToList();

foreach (var snapConversion in snapConversions)
{
int patternStartIndex = 0;

// Skip processing if the corresponding conversion is disabled
if (!shouldProcessRhythm(snapConversion.Key))
continue;

for (int i = 0; i < hits.Count; i++)
{
double snapValue = i < hits.Count - 1
? getSnapBetweenNotes(controlPointInfo, hits[i], hits[i + 1])
: 1; // No next note, default to a safe 1/1 snap

if (snapValue == snapConversion.Key)
{
if (!inPattern)
{
patternStartIndex = i;
}

inPattern = true;
}

// check if end of pattern
if (inPattern && snapValue != snapConversion.Key)
{
// End of the pattern
inPattern = false;

// Iterate through the pattern
for (int j = patternStartIndex; j <= i; j++)
{
int currentHitPosition = j - patternStartIndex;

if (snapConversion.Key == 8)
{
// 1/8: Remove the second note
if (currentHitPosition % 2 == 1)
{
toRemove.Add(hits[j]);
}
}
else
{
// 1/6 and 1/3: Remove the second note and adjust the third
if (currentHitPosition % 3 == 1)
{
toRemove.Add(hits[j]);
}
else if (currentHitPosition % 3 == 2 && j < hits.Count - 1)
{
double offset = controlPointInfo.TimingPointAt(hits[j].StartTime).BeatLength / snapConversion.Value;
hits[j].StartTime = hits[j + 1].StartTime - offset;
}
}
}
}
}

// Remove queued notes
taikoBeatmap.HitObjects.RemoveAll(obj => toRemove.Contains(obj));
}
}

private int getSnapBetweenNotes(ControlPointInfo controlPointInfo, Hit currentNote, Hit nextNote)
{
double gapMs = nextNote.StartTime - currentNote.StartTime;
var currentTimingPoint = controlPointInfo.TimingPointAt(currentNote.StartTime);

return controlPointInfo.GetClosestBeatDivisor(gapMs + currentTimingPoint.Time);
}

private bool shouldProcessRhythm(int snap)
{
return snap switch
{
3 => OneThirdConversion.Value,
6 => OneSixthConversion.Value,
8 => OneEighthConversion.Value,
_ => false,
};
}
}
}
1 change: 1 addition & 0 deletions osu.Game.Rulesets.Taiko/TaikoRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
new TaikoModEasy(),
new TaikoModNoFail(),
new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()),
new TaikoModQuarterize(),
};

case ModType.DifficultyIncrease:
Expand Down
Loading