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 repel mod to the osu ruleset #18607

Merged
merged 23 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;

namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModRepel : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestRepel(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModRepel
{
RepulsionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
});
}
}
}
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawa
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };

private IFrameStableClock gameplayClock;

Expand Down
97 changes: 97 additions & 0 deletions osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be preferable to have all new code have NRT enabled. The problem here is going to be gameplayClock, but I think the way to go here is to annotate it as nullable and then add a Debug.Assert(gameplayClock != null) in easeTo().


using System;
using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
using osu.Game.Rulesets.UI;
using osuTK;

namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModRepel : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Repel";
public override string Acronym => "RP";
public override ModType Type => ModType.Fun;
public override string Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mod is declaring incompatibility with 5 others, but 4 of those 5 are not declaring incompatibility with OsuModRepel themselves in the other direction (OsuModMagnetised being the exception).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still missed autoplay..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and relax.

Copy link
Contributor Author

@ggliv ggliv Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes.

Relax was intentionally excluded though, #17319 isn't an issue when hitobjects are being pushed away. I've opened #19084 to fix.


private IFrameStableClock gameplayClock;

[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
MaxValue = 1.0f,
};

public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
gameplayClock = drawableRuleset.FrameStableClock;

// Hide judgment displays and follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
}

public void Update(Playfield playfield)
{
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;

foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
var destination = Vector2.Clamp(2 * drawable.Position - cursorPos, Vector2.Zero, OsuPlayfield.BASE_SIZE);

if (drawable.HitObject is Slider thisSlider)
{
var possibleMovementBounds = OsuHitObjectGenerationUtils.CalculatePossibleMovementBounds(thisSlider);

destination = Vector2.Clamp(
destination,
new Vector2(possibleMovementBounds.Left, possibleMovementBounds.Top),
new Vector2(possibleMovementBounds.Right, possibleMovementBounds.Bottom)
);
}

switch (drawable)
{
case DrawableHitCircle circle:
easeTo(circle, destination);
break;

case DrawableSlider slider:

if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, destination);
else
easeTo(slider, destination - slider.Ball.DrawPosition);

break;
}
}
}

private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{
double dampLength = Vector2.Distance(hitObject.Position, destination) / (0.04 * RepulsionStrength.Value + 0.04);

float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);

hitObject.Position = new Vector2(x, y);
}
}
}
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/OsuRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
new OsuModMagnetised(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed()
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject)
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{
var slider = (Slider)workingObject.HitObject;
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var possibleMovementBounds = CalculatePossibleMovementBounds(slider);

var previousPosition = workingObject.PositionModified;

Expand Down Expand Up @@ -239,10 +239,12 @@ private static void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <param name="slider">The <see cref="Slider"/> for which to calculate a movement bounding box.</param>
/// <returns>A <see cref="RectangleF"/> which contains all of the possible movements of the slider such that the entire slider is inside the playfield.</returns>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private static RectangleF calculatePossibleMovementBounds(Slider slider)
public static RectangleF CalculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
Expand Down