Skip to content

Implement OsuModDepth #25653

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

Merged
merged 12 commits into from
Dec 25, 2023
132 changes: 132 additions & 0 deletions osu.Game.Rulesets.Osu/Mods/OsuModDepth.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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 System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
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.UI;
using osuTK;

namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDepth : ModWithVisibilityAdjustment, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Depth";
public override string Acronym => "DH";
Copy link
Member

Choose a reason for hiding this comment

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

Maybe DP is better?

public override IconUsage? Icon => FontAwesome.Solid.Cube;
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "3D. Almost.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray();

private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -100);
private readonly float minDepth = depthForScale(1.5f);

[SettingSource("Maximum depth", "How far away objects appear.", 0)]
public BindableFloat MaxDepth { get; } = new BindableFloat(100)
{
Precision = 10,
MinValue = 50,
MaxValue = 200
};

[SettingSource("Show Approach Circles", "Whether approach circles should be visible.", 1)]
public BindableBool ShowApproachCircles { get; } = new BindableBool(true);

protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);

protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);

public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// 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();
}

private void applyTransform(DrawableHitObject drawable, ArmedState state)
{
switch (drawable)
{
case DrawableHitCircle circle:
if (!ShowApproachCircles.Value)
{
var hitObject = (OsuHitObject)drawable.HitObject;
double appearTime = hitObject.StartTime - hitObject.TimePreempt;

using (circle.BeginAbsoluteSequence(appearTime))
circle.ApproachCircle.Hide();
}

break;
}
}

public void Update(Playfield playfield)
{
double time = playfield.Time.Current;

foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
switch (drawable)
{
case DrawableHitCircle circle:
processObject(time, circle, 0);
break;

case DrawableSlider slider:
processObject(time, slider, slider.HitObject.Duration);
break;
}
}
}

private void processObject(double time, DrawableOsuHitObject drawable, double duration)
{
var hitObject = drawable.HitObject;

double baseSpeed = MaxDepth.Value / hitObject.TimePreempt;
double offsetAfterStartTime = duration + hitObject.MaximumJudgementOffset + 500;
double slowSpeed = Math.Min(-minDepth / offsetAfterStartTime, baseSpeed);

double decelerationTime = hitObject.TimePreempt * 0.2;
float decelerationDistance = (float)(decelerationTime * (baseSpeed + slowSpeed) * 0.5);

float z;

if (time < hitObject.StartTime)
{
double timeOffset = time - (hitObject.StartTime - decelerationTime);
double deceleration = (slowSpeed - baseSpeed) / decelerationTime;
z = decelerationDistance - (float)(baseSpeed * timeOffset + deceleration * timeOffset * timeOffset * 0.5);
}
else
Copy link
Member

Choose a reason for hiding this comment

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

My proposal was to only slow objects down when actually required (even if applied to all sliders to start with). I think not applying this whenever we can avoid doing so will make the visuals of this mod much better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah okay, we are on the same page then

Copy link
Member

Choose a reason for hiding this comment

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

Even better would be only applying when a slider gets "too long" (maybe too long for autoplay to hit it correctly with no autoplay path adjustment?)

Copy link
Contributor Author

@EVAST9919 EVAST9919 Dec 6, 2023

Choose a reason for hiding this comment

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

So I've made some changes. Now every object moves at a constant speed, except for long sliders. There's a check for how big it would become at the end time if allowed to move at a constant speed. If its scale will be bigger than 1.5 deceleration will be applied.
Generally works fine for autoplay due to followcircle being big enough for cursor to follow.

{
double endTime = hitObject.StartTime + offsetAfterStartTime;
z = -(float)((Math.Min(time, endTime) - hitObject.StartTime) * slowSpeed);
}

float scale = scaleForDepth(z);
drawable.Position = toPlayfieldPosition(scale, hitObject.Position);
drawable.Scale = new Vector2(scale);
}

private static float scaleForDepth(float depth) => 100 / (depth - camera_position.Z);

private static float depthForScale(float scale) => 100 / scale + camera_position.Z;

private static Vector2 toPlayfieldPosition(float scale, Vector2 positionAtZeroDepth)
{
return (positionAtZeroDepth - camera_position.Xy) * scale + camera_position.Xy;
}
}
}
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class OsuModFreezeFrame : Mod, IApplicableToDrawableHitObject, IApplicabl
public override LocalisableString Description => "Burn the notes into your memory.";

//Alters the transforms of the approach circles, breaking the effects of these mods.
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth) }).ToArray();

public override ModType Type => ModType.Fun;

Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class OsuModHidden : ModHidden, IHidesApproachCircles
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;

public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) };

public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
Expand Down
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 @@ -27,7 +27,7 @@ internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawa
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) };

[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IHid

protected virtual float EndScale => 1;

public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween), typeof(OsuModDepth) };

protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal class OsuModRepel : Mod, IUpdatableByPlayfield, IApplicableToDrawableRu
public override ModType Type => ModType.Fun;
public override LocalisableString 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), typeof(OsuModBubbles) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles), typeof(OsuModDepth) };

[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class OsuModSpinIn : ModWithVisibilityAdjustment, IHidesApproachCircles

// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
// further implementation will be required for supporting that.
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModDepth) };

private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
Expand Down
3 changes: 2 additions & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDr
typeof(OsuModRandom),
typeof(OsuModSpunOut),
typeof(OsuModStrictTracking),
typeof(OsuModSuddenDeath)
typeof(OsuModSuddenDeath),
typeof(OsuModDepth)
}).ToArray();

[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCir
public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;

public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };

protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class OsuModTransform : ModWithVisibilityAdjustment
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(OsuModDepth) }).ToArray();

private float theta;

Expand Down
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal class OsuModWiggle : ModWithVisibilityAdjustment
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModDepth) };

private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles

Expand Down
3 changes: 2 additions & 1 deletion osu.Game.Rulesets.Osu/OsuRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
new ModAdaptiveSpeed(),
new OsuModFreezeFrame(),
new OsuModBubbles(),
new OsuModSynesthesia()
new OsuModSynesthesia(),
new OsuModDepth()
};

case ModType.System:
Expand Down