From 96edf55b6dc17b5fe20f39df8370987806981a51 Mon Sep 17 00:00:00 2001 From: Tenexxt Date: Thu, 19 Mar 2026 23:31:06 +0100 Subject: [PATCH 1/4] Implement base for `Bad Apple` mod Co-Authored-By: maarvin <8039761+minetoblend@users.noreply.github.com> --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 1 + .../Mods/CatchModBadApple.cs | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index eb8cf137faf7..427a16bc0049 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -155,6 +155,7 @@ public override IEnumerable GetModsFor(ModType type) new CatchModMuted(), new CatchModNoScope(), new CatchModMovingFast(), + new CatchModBadApple(), }; case ModType.System: diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs new file mode 100644 index 000000000000..f1cdf4e8976c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs @@ -0,0 +1,38 @@ +// 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 osu.Framework.Localisation; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Catch.Mods +{ + public class CatchModBadApple : Mod, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset + { + public override string Name => "Bad Apple"; + public override LocalisableString Description => "Dodge the beat!"; + public override double ScoreMultiplier => 1; + public override string Acronym => "BA"; + public override Type[] IncompatibleMods => new[] { typeof(CatchModAutoplay) }; + + private Catcher catcher = null!; + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + catcher = ((CatchPlayfield)drawableRuleset.Playfield).Catcher; + } + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + if (drawable is DrawableCatchHitObject catchHitObject) + { + catchHitObject.CheckPosition = hitObject => !catcher.CanCatch(hitObject); + } + } + } +} From 2df84622ab12cd479d24f79f5162ddb5a86fcdb4 Mon Sep 17 00:00:00 2001 From: Tenexxt Date: Sun, 22 Mar 2026 00:34:04 +0100 Subject: [PATCH 2/4] Block Hyperdashes and don't show caught fruits --- osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs index f1cdf4e8976c..84d3c095b3a6 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModBadApple : Mod, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset + public class CatchModBadApple : Mod, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset, IUpdatableByPlayfield { public override string Name => "Bad Apple"; public override LocalisableString Description => "Dodge the beat!"; @@ -25,6 +25,9 @@ public class CatchModBadApple : Mod, IApplicableToDrawableHitObject, IApplicable public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { catcher = ((CatchPlayfield)drawableRuleset.Playfield).Catcher; + + // Don't show caught fruits as they aren't technically being caught. + catcher.CatchFruitOnPlate = false; } public void ApplyToDrawableHitObject(DrawableHitObject drawable) @@ -34,5 +37,11 @@ public void ApplyToDrawableHitObject(DrawableHitObject drawable) catchHitObject.CheckPosition = hitObject => !catcher.CanCatch(hitObject); } } + + public void Update(Playfield playfield) + { + // Block hyperdashing to avoid hyperdashes when two objects appear at the same time. + catcher.SetHyperDashState(1, -1); + } } } From 7c669bd9b93dcf5cc308b851b8b2d043ba155102 Mon Sep 17 00:00:00 2001 From: Tenexxt Date: Sun, 22 Mar 2026 15:10:47 +0100 Subject: [PATCH 3/4] Add `Mute hit sounds` mod setting --- osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs index 84d3c095b3a6..ad71619ab6c5 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Localisation; +using osu.Framework.Audio; +using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; @@ -20,7 +23,11 @@ public class CatchModBadApple : Mod, IApplicableToDrawableHitObject, IApplicable public override string Acronym => "BA"; public override Type[] IncompatibleMods => new[] { typeof(CatchModAutoplay) }; + [SettingSource("Mute hit sounds", "Hit sounds become muted.")] + public BindableBool AffectsHitSounds { get; } = new BindableBool(true); + private Catcher catcher = null!; + private readonly BindableNumber hitSoundVolume = new BindableDouble(0); public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { @@ -28,6 +35,11 @@ public void ApplyToDrawableRuleset(DrawableRuleset drawableRules // Don't show caught fruits as they aren't technically being caught. catcher.CatchFruitOnPlate = false; + + if (AffectsHitSounds.Value) + { + drawableRuleset.Audio.AddAdjustment(AdjustableProperty.Volume, hitSoundVolume); + } } public void ApplyToDrawableHitObject(DrawableHitObject drawable) From e2bb407a8153fe272b136ed615d920430e60fae6 Mon Sep 17 00:00:00 2001 From: Tenexxt Date: Tue, 24 Mar 2026 23:38:10 +0100 Subject: [PATCH 4/4] Allow fruits to continue falling after cought \\ don't review this yet also now there is a test scene i guess --- .../Mods/TestSceneCatchModBadApple.cs | 21 +++++++++++++++++++ .../Mods/CatchModBadApple.cs | 14 +++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModBadApple.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModBadApple.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModBadApple.cs new file mode 100644 index 000000000000..1a2dea4d55e5 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModBadApple.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests.Mods +{ + public partial class TestSceneCatchModBadApple : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + [Test] + public void TestBadApple() => CreateModTest(new ModTestData + { + Mod = new CatchModBadApple(), + PassCondition = () => true + }); + } +} diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs index ad71619ab6c5..c8afbe3d2a93 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModBadApple.cs @@ -12,13 +12,14 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +using osu.Framework.Graphics; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModBadApple : Mod, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset, IUpdatableByPlayfield { public override string Name => "Bad Apple"; - public override LocalisableString Description => "Dodge the beat!"; + public override LocalisableString Description => "The fruit has gone bad... dodge it!"; public override double ScoreMultiplier => 1; public override string Acronym => "BA"; public override Type[] IncompatibleMods => new[] { typeof(CatchModAutoplay) }; @@ -48,8 +49,17 @@ public void ApplyToDrawableHitObject(DrawableHitObject drawable) { catchHitObject.CheckPosition = hitObject => !catcher.CanCatch(hitObject); } - } + drawable.ApplyCustomUpdateState += (dho, state) => + { + // Keep the existing transforms when hit. + if (state is not ArmedState.Hit) + return; + + // When "hit", the DHO is faded out, so to let fruits fall after being caught we fade them back in. + dho.FadeIn(); + }; + } public void Update(Playfield playfield) { // Block hyperdashing to avoid hyperdashes when two objects appear at the same time.