2021-06-24 13:19:42 +08:00
// 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.
2022-06-17 15:37:17 +08:00
#nullable disable
2021-06-24 13:19:42 +08:00
using System ;
2021-07-26 22:46:41 +08:00
using System.Linq ;
using osu.Framework.Extensions.IEnumerableExtensions ;
2021-06-24 13:19:42 +08:00
using osu.Game.Rulesets.Osu.UI ;
2021-07-26 22:46:41 +08:00
using osu.Game.Rulesets.Objects ;
2022-06-19 19:07:10 +08:00
using osu.Game.Rulesets.Osu.Beatmaps ;
2021-07-26 22:46:41 +08:00
using osu.Game.Rulesets.Osu.Objects ;
2021-06-24 13:19:42 +08:00
using osuTK ;
namespace osu.Game.Rulesets.Osu.Utils
{
2022-03-10 11:53:03 +08:00
public static partial class OsuHitObjectGenerationUtils
2021-06-24 13:19:42 +08:00
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
private const float playfield_edge_ratio = 0.375f ;
private static readonly float border_distance_x = OsuPlayfield . BASE_SIZE . X * playfield_edge_ratio ;
private static readonly float border_distance_y = OsuPlayfield . BASE_SIZE . Y * playfield_edge_ratio ;
private static readonly Vector2 playfield_middle = OsuPlayfield . BASE_SIZE / 2 ;
/// <summary>
/// Rotate a hit object away from the playfield edge, while keeping a constant distance
/// from the previous object.
/// </summary>
/// <remarks>
/// The extent of rotation depends on the position of the hit object. Hit objects
/// closer to the playfield edge will be rotated to a larger extent.
/// </remarks>
/// <param name="prevObjectPos">Position of the previous hit object.</param>
/// <param name="posRelativeToPrev">Position of the hit object to be rotated, relative to the previous hit object.</param>
/// <param name="rotationRatio">
/// The extent of rotation.
/// 0 means the hit object is never rotated.
/// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge.
/// </param>
/// <returns>The new position of the hit object, relative to the previous one.</returns>
public static Vector2 RotateAwayFromEdge ( Vector2 prevObjectPos , Vector2 posRelativeToPrev , float rotationRatio = 0.5f )
{
2021-10-27 12:04:41 +08:00
float relativeRotationDistance = 0f ;
2021-06-24 13:19:42 +08:00
if ( prevObjectPos . X < playfield_middle . X )
{
relativeRotationDistance = Math . Max (
( border_distance_x - prevObjectPos . X ) / border_distance_x ,
relativeRotationDistance
) ;
}
else
{
relativeRotationDistance = Math . Max (
( prevObjectPos . X - ( OsuPlayfield . BASE_SIZE . X - border_distance_x ) ) / border_distance_x ,
relativeRotationDistance
) ;
}
if ( prevObjectPos . Y < playfield_middle . Y )
{
relativeRotationDistance = Math . Max (
( border_distance_y - prevObjectPos . Y ) / border_distance_y ,
relativeRotationDistance
) ;
}
else
{
relativeRotationDistance = Math . Max (
( prevObjectPos . Y - ( OsuPlayfield . BASE_SIZE . Y - border_distance_y ) ) / border_distance_y ,
relativeRotationDistance
) ;
}
2021-06-24 13:22:10 +08:00
return RotateVectorTowardsVector (
posRelativeToPrev ,
playfield_middle - prevObjectPos ,
Math . Min ( 1 , relativeRotationDistance * rotationRatio )
) ;
2021-06-24 13:19:42 +08:00
}
/// <summary>
/// Rotates vector "initial" towards vector "destination".
/// </summary>
/// <param name="initial">The vector to be rotated.</param>
/// <param name="destination">The vector that "initial" should be rotated towards.</param>
/// <param name="rotationRatio">How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination".</param>
/// <returns>The rotated vector.</returns>
public static Vector2 RotateVectorTowardsVector ( Vector2 initial , Vector2 destination , float rotationRatio )
{
2021-10-27 12:04:41 +08:00
float initialAngleRad = MathF . Atan2 ( initial . Y , initial . X ) ;
float destAngleRad = MathF . Atan2 ( destination . Y , destination . X ) ;
2021-06-24 13:19:42 +08:00
2021-10-27 12:04:41 +08:00
float diff = destAngleRad - initialAngleRad ;
2021-06-24 13:19:42 +08:00
2021-06-25 09:44:23 +08:00
while ( diff < - MathF . PI ) diff + = 2 * MathF . PI ;
2021-06-24 13:19:42 +08:00
2021-06-25 09:44:23 +08:00
while ( diff > MathF . PI ) diff - = 2 * MathF . PI ;
2021-06-24 13:19:42 +08:00
2021-10-27 12:04:41 +08:00
float finalAngleRad = initialAngleRad + rotationRatio * diff ;
2021-06-24 13:19:42 +08:00
return new Vector2 (
2021-06-25 09:44:23 +08:00
initial . Length * MathF . Cos ( finalAngleRad ) ,
initial . Length * MathF . Sin ( finalAngleRad )
2021-06-24 13:19:42 +08:00
) ;
}
2021-07-26 22:46:41 +08:00
/// <summary>
2021-07-27 23:22:32 +08:00
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield horizontally.
2021-07-26 22:46:41 +08:00
/// </summary>
2021-07-27 21:01:01 +08:00
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectHorizontally ( OsuHitObject osuObject )
2021-07-26 22:46:41 +08:00
{
osuObject . Position = new Vector2 ( OsuPlayfield . BASE_SIZE . X - osuObject . X , osuObject . Position . Y ) ;
2022-12-07 08:05:15 +08:00
if ( osuObject is not Slider slider )
2021-07-27 21:01:01 +08:00
return ;
2021-07-26 22:46:41 +08:00
2022-12-07 08:05:15 +08:00
FlipSliderHorizontally ( slider ) ;
2021-07-26 22:46:41 +08:00
}
/// <summary>
2021-07-27 23:22:32 +08:00
/// Reflects the position of the <see cref="OsuHitObject"/> in the playfield vertically.
2021-07-26 22:46:41 +08:00
/// </summary>
2021-07-27 21:01:01 +08:00
/// <param name="osuObject">The object to reflect.</param>
public static void ReflectVertically ( OsuHitObject osuObject )
2021-07-26 22:46:41 +08:00
{
osuObject . Position = new Vector2 ( osuObject . Position . X , OsuPlayfield . BASE_SIZE . Y - osuObject . Y ) ;
2022-12-07 08:05:15 +08:00
if ( osuObject is not Slider slider )
2021-07-27 21:01:01 +08:00
return ;
2021-07-26 22:46:41 +08:00
2022-12-08 04:20:11 +08:00
void flipNestedObject ( OsuHitObject nested ) = > nested . Position = new Vector2 ( nested . Position . X , OsuPlayfield . BASE_SIZE . Y - nested . Position . Y ) ;
2022-12-07 08:05:15 +08:00
static void flipControlPoint ( PathControlPoint point ) = > point . Position = new Vector2 ( point . Position . X , - point . Position . Y ) ;
2021-07-26 22:46:41 +08:00
2022-12-07 08:05:15 +08:00
modifySlider ( slider , flipNestedObject , flipControlPoint ) ;
2021-07-26 22:46:41 +08:00
}
2022-04-01 11:36:20 +08:00
/// <summary>
/// Rotate a slider about its start position by the specified angle.
/// </summary>
/// <param name="slider">The slider to be rotated.</param>
2022-04-17 10:40:43 +08:00
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
2022-04-01 11:36:20 +08:00
public static void RotateSlider ( Slider slider , float rotation )
{
void rotateNestedObject ( OsuHitObject nested ) = > nested . Position = rotateVector ( nested . Position - slider . Position , rotation ) + slider . Position ;
2022-12-07 07:40:18 +08:00
void rotateControlPoint ( PathControlPoint point ) = > point . Position = rotateVector ( point . Position , rotation ) ;
2022-04-01 11:36:20 +08:00
2022-12-07 07:40:18 +08:00
modifySlider ( slider , rotateNestedObject , rotateControlPoint ) ;
}
/// <summary>
2022-12-07 07:48:25 +08:00
/// Flips the slider about its start position horizontally.
2022-12-07 07:40:18 +08:00
/// </summary>
2022-12-07 07:48:25 +08:00
public static void FlipSliderHorizontally ( Slider slider )
2022-12-07 07:40:18 +08:00
{
2022-12-07 08:05:15 +08:00
void flipNestedObject ( OsuHitObject nested ) = > nested . Position = new Vector2 ( slider . X - ( nested . X - slider . X ) , nested . Y ) ;
2022-12-07 07:40:18 +08:00
static void flipControlPoint ( PathControlPoint point ) = > point . Position = new Vector2 ( - point . Position . X , point . Position . Y ) ;
modifySlider ( slider , flipNestedObject , flipControlPoint ) ;
}
private static void modifySlider ( Slider slider , Action < OsuHitObject > modifyNestedObject , Action < PathControlPoint > modifyControlPoint )
{
2022-04-18 09:38:51 +08:00
// No need to update the head and tail circles, since slider handles that when the new slider path is set
2022-12-07 07:40:18 +08:00
slider . NestedHitObjects . OfType < SliderTick > ( ) . ForEach ( modifyNestedObject ) ;
slider . NestedHitObjects . OfType < SliderRepeat > ( ) . ForEach ( modifyNestedObject ) ;
2022-04-01 11:36:20 +08:00
var controlPoints = slider . Path . ControlPoints . Select ( p = > new PathControlPoint ( p . Position , p . Type ) ) . ToArray ( ) ;
foreach ( var point in controlPoints )
2022-12-07 07:40:18 +08:00
modifyControlPoint ( point ) ;
2022-04-01 11:36:20 +08:00
slider . Path = new SliderPath ( controlPoints , slider . Path . ExpectedDistance . Value ) ;
}
/// <summary>
/// Rotate a vector by the specified angle.
/// </summary>
/// <param name="vector">The vector to be rotated.</param>
2022-04-17 10:40:43 +08:00
/// <param name="rotation">The angle, measured in radians, to rotate the vector by.</param>
2022-04-01 11:36:20 +08:00
/// <returns>The rotated vector.</returns>
private static Vector2 rotateVector ( Vector2 vector , float rotation )
{
2022-04-11 14:15:08 +08:00
float angle = MathF . Atan2 ( vector . Y , vector . X ) + rotation ;
2022-04-01 11:36:20 +08:00
float length = vector . Length ;
return new Vector2 (
2022-04-11 14:15:08 +08:00
length * MathF . Cos ( angle ) ,
length * MathF . Sin ( angle )
2022-04-01 11:36:20 +08:00
) ;
}
2022-06-19 19:07:10 +08:00
2022-06-20 02:43:17 +08:00
/// <param name="beatmap">The beatmap hitObject is a part of.</param>
/// <param name="hitObject">The <see cref="OsuHitObject"/> that should be checked.</param>
/// <param name="downbeatsOnly">If true, this method only returns true if hitObject is on a downbeat.
/// If false, it returns true if hitObject is on any beat.</param>
/// <returns>true if hitObject is on a (down-)beat, false otherwise.</returns>
2022-06-19 19:07:10 +08:00
public static bool IsHitObjectOnBeat ( OsuBeatmap beatmap , OsuHitObject hitObject , bool downbeatsOnly = false )
{
2022-09-09 17:00:51 +08:00
var timingPoint = beatmap . ControlPointInfo . TimingPointAt ( hitObject . StartTime ) ;
2022-06-19 19:07:10 +08:00
2022-09-09 17:00:51 +08:00
double timeSinceTimingPoint = hitObject . StartTime - timingPoint . Time ;
2022-06-19 19:07:10 +08:00
2022-09-09 17:00:51 +08:00
double beatLength = timingPoint . BeatLength ;
2022-06-19 19:07:10 +08:00
2022-09-09 17:00:51 +08:00
if ( downbeatsOnly )
beatLength * = timingPoint . TimeSignature . Numerator ;
2022-06-19 19:07:10 +08:00
2022-09-09 17:00:51 +08:00
// Ensure within 1ms of expected location.
return Math . Abs ( timeSinceTimingPoint + 1 ) % beatLength < 2 ;
2022-06-19 19:07:10 +08:00
}
2022-06-22 22:49:07 +08:00
/// <summary>
/// Generates a random number from a normal distribution using the Box-Muller transform.
/// </summary>
2022-06-19 19:07:10 +08:00
public static float RandomGaussian ( Random rng , float mean = 0 , float stdDev = 1 )
{
2022-06-22 22:49:07 +08:00
// Generate 2 random numbers in the interval (0,1].
// x1 must not be 0 since log(0) = undefined.
2022-06-20 06:19:29 +08:00
double x1 = 1 - rng . NextDouble ( ) ;
double x2 = 1 - rng . NextDouble ( ) ;
2022-06-22 22:49:07 +08:00
2022-06-20 05:03:41 +08:00
double stdNormal = Math . Sqrt ( - 2 * Math . Log ( x1 ) ) * Math . Sin ( 2 * Math . PI * x2 ) ;
2022-06-19 19:07:10 +08:00
return mean + stdDev * ( float ) stdNormal ;
}
2021-06-24 13:19:42 +08:00
}
}